├── .gitignore ├── CHANGELOG.md ├── COPYING ├── Contributors.md ├── README.md ├── build.gradle ├── buildSrc ├── build.gradle └── src │ └── main │ └── groovy │ ├── multiloader-common.gradle │ └── multiloader-loader.gradle ├── common ├── build.gradle └── src │ └── main │ ├── java │ └── net │ │ └── minescript │ │ └── common │ │ ├── BlockPack.java │ │ ├── BlockPacker.java │ │ ├── ChunkLoadEventListener.java │ │ ├── CommandSyntax.java │ │ ├── Config.java │ │ ├── EntityExporter.java │ │ ├── EntitySelection.java │ │ ├── ExceptionInfo.java │ │ ├── FunctionExecutor.java │ │ ├── Job.java │ │ ├── JobControl.java │ │ ├── JobState.java │ │ ├── Math3d.java │ │ ├── Message.java │ │ ├── Minescript.java │ │ ├── Numbers.java │ │ ├── Platform.java │ │ ├── ResourceTracker.java │ │ ├── ScriptConfig.java │ │ ├── ScriptFunctionCall.java │ │ ├── ScriptFunctionRunner.java │ │ ├── ScriptRedirect.java │ │ ├── SubprocessTask.java │ │ ├── SystemMessageQueue.java │ │ ├── Task.java │ │ └── mixin │ │ ├── ChatComponentMixin.java │ │ ├── ChatScreenMixin.java │ │ ├── ClientPacketListenerMixin.java │ │ ├── KeyMappingMixin.java │ │ ├── KeyboardHandlerMixin.java │ │ └── MouseHandlerMixin.java │ └── resources │ ├── COPYING │ ├── copy_blocks.py │ ├── eval.py │ ├── help.py │ ├── minescript.mixins.json │ ├── minescript.py │ ├── minescript_runtime.py │ ├── pack.mcmeta │ ├── paste.py │ ├── posix_config.txt │ ├── version.txt │ └── windows_config.txt ├── docs ├── README.md ├── v1.19 │ └── README.md ├── v2.0 │ └── README.md ├── v2.1 │ └── README.md ├── v3.0 │ └── README.md ├── v3.1 │ └── README.md └── v3.2 │ └── README.md ├── fabric ├── .gitignore ├── build.gradle └── src │ └── main │ ├── java │ └── net │ │ └── minescript │ │ └── fabric │ │ ├── FabricPlatform.java │ │ ├── MinescriptFabricClientMod.java │ │ └── MinescriptFabricMod.java │ └── resources │ ├── assets │ └── modid │ │ └── minescript-logo.png │ ├── fabric.mod.json │ └── minescript.fabric.mixins.json ├── forge ├── .gitattributes ├── .gitignore ├── build.gradle └── src │ └── main │ ├── java │ └── net │ │ └── minescript │ │ └── forge │ │ ├── Constants.java │ │ ├── ForgePlatform.java │ │ ├── MinescriptForgeClientMod.java │ │ ├── MinescriptForgeMod.java │ │ └── mixin │ │ └── LevelRendererMixin.java │ └── resources │ ├── META-INF │ └── mods.toml │ ├── minescript-logo.png │ └── minescript.forge.mixins.json ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── neoforge ├── build.gradle └── src │ └── main │ ├── java │ └── net │ │ └── minescript │ │ └── neoforge │ │ ├── Constants.java │ │ ├── MinescriptNeoForgeClientMod.java │ │ ├── MinescriptNeoForgeMod.java │ │ └── NeoForgePlatform.java │ └── resources │ ├── META-INF │ └── neoforge.mods.toml │ └── minescript-logo.png ├── settings.gradle ├── test └── minescript_test.py └── tools ├── find_version_number.sh ├── markdown_to_html.py ├── pydoc_to_markdown.py ├── split_text_to_chat_width.py └── update_version_number.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # vim 2 | .*.swp 3 | 4 | # macOS 5 | .DS_Store 6 | 7 | # eclipse 8 | bin 9 | *.launch 10 | .settings 11 | .metadata 12 | .classpath 13 | .project 14 | 15 | # idea 16 | out 17 | *.ipr 18 | *.iws 19 | *.iml 20 | .idea/* 21 | !.idea/scopes 22 | 23 | # gradle 24 | build 25 | .gradle 26 | 27 | # other 28 | eclipse 29 | run 30 | runs 31 | .vscode 32 | -------------------------------------------------------------------------------- /Contributors.md: -------------------------------------------------------------------------------- 1 | [MCT32](https://github.com/mct32) 2 | [Supernova125](https://github.com/supernova125) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minescript 2 | 3 | ## Introduction 4 | 5 | **Minescript** is a platform for controlling and interacting with Minecraft using scripts written in 6 | Python and other scripting languages. It is implemented as mod for [Fabric](https://fabricmc.net/), 7 | [Forge](https://files.minecraftforge.net/net/minecraftforge/forge/), and 8 | [NeoForge](https://neoforged.net/). 9 | 10 | The examples below require Minescript 4.0 or higher. 11 | 12 | ## How it works 13 | 14 | Place Python scripts (`.py` files) in the `minescript` folder (located inside the `minecraft` 15 | folder) to run them from the Minecraft chat console. A file at `minecraft/minescript/example.py` 16 | can be executed from the Minecraft chat as: 17 | 18 | ``` 19 | \example 20 | ``` 21 | 22 | `minescript.py` is a script library that's automatically installed in the 23 | `minecraft/minescript/system/lib` folder the first time running Minecraft with the Minescript mod 24 | installed. `minescript.py` contains a library of functions for accessing Minecraft functionality: 25 | 26 | ``` 27 | # example.py: 28 | 29 | import minescript 30 | 31 | # Write a message to the chat that only you can see: 32 | minescript.echo("Hello, world!") 33 | 34 | # Write a chat message that other players can see: 35 | minescript.chat("Hello, everyone!") 36 | 37 | # Get your player's current position: 38 | x, y, z = minescript.player().position 39 | 40 | # Print information for the block that your player is standing on: 41 | minescript.echo(minescript.getblock(x, y - 1, z)) 42 | 43 | # Set the block directly beneath your player (assuming commands are enabled): 44 | x, y, z = int(x), int(y), int(z) 45 | minescript.execute(f"setblock {x} {y-1} {z} yellow_concrete") 46 | 47 | # Display the contents of your inventory: 48 | for item in minescript.player_inventory(): 49 | minescript.echo(item.item) 50 | ``` 51 | 52 | ## Pre-built mod jars 53 | 54 | Pre-built mod jars for Fabric, Forge, and NeoForge can be downloaded from 55 | [Modrinth](https://modrinth.com/mod/minescript/versions) and 56 | [CurseForge](https://www.curseforge.com/minecraft/mc-mods/minescript/files). 57 | 58 | ## Command-line build instructions 59 | 60 | To run the mod in dev mode, clone this repo: 61 | 62 | ``` 63 | $ git clone https://github.com/maxuser0/minescript.git 64 | ``` 65 | 66 | Then run the dev client for one of the supported mod loaders: 67 | 68 | ``` 69 | # Fabric client: 70 | $ ./gradlew fabric:runClient 71 | 72 | # Forge client: 73 | $ ./gradlew forge:runClient 74 | 75 | # NeoForge client: 76 | $ ./gradlew neoforge:runClient 77 | ``` 78 | 79 | To build the mod without running it in dev mode, run: 80 | 81 | ``` 82 | # Build the Fabric mod: 83 | $ ./gradlew fabric:build 84 | 85 | # Build the Forge mod: 86 | $ ./gradlew forge:build 87 | 88 | # Build the NeoForge mod: 89 | $ ./gradlew neoforge:build 90 | ``` 91 | 92 | The built mod jars will appear in `build/libs` within the given mod platform's subdirectory, e.g. 93 | 94 | ``` 95 | $ ls */build/libs/*-4.0.jar 96 | fabric/build/libs/minescript-fabric-1.21.1-4.0.jar 97 | forge/build/libs/minescript-forge-1.21.1-4.0.jar 98 | neoforge/build/libs/minescript-neoforge-1.21.1-4.0.jar 99 | ``` 100 | 101 | ## License 102 | 103 | All code, compiled or in source form, in the built mod jar is licensed as GPL 104 | v3 (specifically `SPDX-License-Identifier: GPL-3.0-only`). Sources within the 105 | `tools` directory that are not distributed in the mod jar and are not required 106 | for building or running the mod jar may be covered by a different license. 107 | 108 | ## Credits 109 | 110 | Special thanks to **Spiderfffun** and **Coolbou0427** for extensive testing on Windows. 111 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Based on MultiLoader-Template: 2 | // https://github.com/jaredlll08/MultiLoader-Template/blob/1.21/build.gradle 3 | plugins { 4 | // see https://fabricmc.net/develop/ for new versions 5 | id 'fabric-loom' version '1.10-SNAPSHOT' apply false 6 | // see https://projects.neoforged.net/neoforged/moddevgradle for new versions 7 | id 'net.neoforged.moddev' version '2.0.42-beta' apply false 8 | } 9 | -------------------------------------------------------------------------------- /buildSrc/build.gradle: -------------------------------------------------------------------------------- 1 | // Based on MultiLoader-Template: 2 | // https://github.com/jaredlll08/MultiLoader-Template/blob/1.21/buildSrc/build.gradle 3 | plugins { 4 | id 'groovy-gradle-plugin' 5 | } 6 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/multiloader-common.gradle: -------------------------------------------------------------------------------- 1 | // Based on MultiLoader-Template: 2 | // https://github.com/jaredlll08/MultiLoader-Template/blob/1.21/buildSrc/src/main/groovy/multiloader-common.gradle 3 | plugins { 4 | id 'java-library' 5 | id 'maven-publish' 6 | } 7 | 8 | base { 9 | archivesName = "${mod_id}-${project.name}-${minecraft_version}" 10 | } 11 | 12 | java { 13 | toolchain.languageVersion = JavaLanguageVersion.of(java_version) 14 | withSourcesJar() 15 | withJavadocJar() 16 | } 17 | 18 | repositories { 19 | mavenCentral() 20 | // https://docs.gradle.org/current/userguide/declaring_repositories.html#declaring_content_exclusively_found_in_one_repository 21 | exclusiveContent { 22 | forRepository { 23 | maven { 24 | name = 'Sponge' 25 | url = 'https://repo.spongepowered.org/repository/maven-public' 26 | } 27 | } 28 | filter { includeGroupAndSubgroups('org.spongepowered') } 29 | } 30 | exclusiveContent { 31 | forRepository { 32 | maven { 33 | name = 'ParchmentMC' 34 | url = 'https://maven.parchmentmc.org/' 35 | } 36 | } 37 | filter { includeGroup('org.parchmentmc.data') } 38 | } 39 | maven { 40 | name = 'BlameJared' 41 | url = 'https://maven.blamejared.com' 42 | } 43 | } 44 | 45 | // Declare capabilities on the outgoing configurations. 46 | // Read more about capabilities here: https://docs.gradle.org/current/userguide/component_capabilities.html#sec:declaring-additional-capabilities-for-a-local-component 47 | ['apiElements', 'runtimeElements', 'sourcesElements', 'javadocElements'].each { variant -> 48 | configurations."$variant".outgoing { 49 | capability("$group:${base.archivesName.get()}:$version") 50 | capability("$group:$mod_id-${project.name}-${minecraft_version}:$version") 51 | capability("$group:$mod_id:$version") 52 | } 53 | publishing.publications.configureEach { 54 | suppressPomMetadataWarningsFor(variant) 55 | } 56 | } 57 | 58 | sourcesJar { 59 | from(rootProject.file('LICENSE')) { 60 | rename { "${it}_${mod_name}" } 61 | } 62 | } 63 | 64 | jar { 65 | from(rootProject.file('LICENSE')) { 66 | rename { "${it}_${mod_name}" } 67 | } 68 | 69 | manifest { 70 | attributes([ 71 | 'Specification-Title' : mod_name, 72 | 'Specification-Vendor' : mod_author, 73 | 'Specification-Version' : project.jar.archiveVersion, 74 | 'Implementation-Title' : project.name, 75 | 'Implementation-Version': project.jar.archiveVersion, 76 | 'Implementation-Vendor' : mod_author, 77 | 'Built-On-Minecraft' : minecraft_version 78 | ]) 79 | } 80 | } 81 | 82 | processResources { 83 | var expandProps = [ 84 | 'version': version, 85 | 'group': project.group, //Else we target the task's group. 86 | 'minecraft_version': minecraft_version, 87 | 'minecraft_version_range': minecraft_version_range, 88 | 'fabric_version': fabric_version, 89 | 'fabric_loader_version': fabric_loader_version, 90 | 'mod_name': mod_name, 91 | 'mod_author': mod_author, 92 | 'mod_id': mod_id, 93 | 'license': license, 94 | 'description': project.description, 95 | 'neoforge_version': neoforge_version, 96 | 'neoforge_loader_version_range': neoforge_loader_version_range, 97 | "forge_version": forge_version, 98 | "forge_loader_version_range": forge_loader_version_range, 99 | 'credits': credits, 100 | 'java_version': java_version 101 | ] 102 | 103 | filesMatching(['version.txt', 'pack.mcmeta', 'fabric.mod.json', 'META-INF/mods.toml', 'META-INF/neoforge.mods.toml', '*.mixins.json']) { 104 | expand expandProps 105 | } 106 | inputs.properties(expandProps) 107 | } 108 | 109 | publishing { 110 | publications { 111 | register('mavenJava', MavenPublication) { 112 | artifactId base.archivesName.get() 113 | from components.java 114 | } 115 | } 116 | repositories { 117 | maven { 118 | url System.getenv('local_maven_url') 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/multiloader-loader.gradle: -------------------------------------------------------------------------------- 1 | // Based on MultiLoader-Template: 2 | // https://github.com/jaredlll08/MultiLoader-Template/blob/1.21/buildSrc/src/main/groovy/multiloader-loader.gradle 3 | plugins { 4 | id 'multiloader-common' 5 | } 6 | 7 | configurations { 8 | commonJava{ 9 | canBeResolved = true 10 | } 11 | commonResources{ 12 | canBeResolved = true 13 | } 14 | } 15 | 16 | dependencies { 17 | compileOnly(project(':common')) { 18 | capabilities { 19 | requireCapability "$group:$mod_id" 20 | } 21 | } 22 | commonJava project(path: ':common', configuration: 'commonJava') 23 | commonResources project(path: ':common', configuration: 'commonResources') 24 | } 25 | 26 | tasks.named('compileJava', JavaCompile) { 27 | dependsOn(configurations.commonJava) 28 | source(configurations.commonJava) 29 | } 30 | 31 | processResources { 32 | dependsOn(configurations.commonResources) 33 | from(configurations.commonResources) 34 | } 35 | 36 | tasks.named('javadoc', Javadoc).configure { 37 | dependsOn(configurations.commonJava) 38 | source(configurations.commonJava) 39 | } 40 | 41 | tasks.named('sourcesJar', Jar) { 42 | dependsOn(configurations.commonJava) 43 | from(configurations.commonJava) 44 | dependsOn(configurations.commonResources) 45 | from(configurations.commonResources) 46 | } 47 | -------------------------------------------------------------------------------- /common/build.gradle: -------------------------------------------------------------------------------- 1 | // Based on MultiLoader-Template: 2 | // https://github.com/jaredlll08/MultiLoader-Template/blob/1.21/common/build.gradle 3 | plugins { 4 | id 'multiloader-common' 5 | id 'net.neoforged.moddev' 6 | } 7 | 8 | neoForge { 9 | neoFormVersion = neo_form_version 10 | // Automatically enable AccessTransformers if the file exists 11 | // While this location can be changed, it is recommended for 12 | // common and neoforge to share an accesstransformer file 13 | // and this location is hardcoded in FML 14 | // https://github.com/neoforged/FancyModLoader/blob/a952595eaaddd571fbc53f43847680b00894e0c1/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFile.java#L118 15 | def at = file('src/main/resources/META-INF/accesstransformer.cfg') 16 | if (at.exists()) { 17 | accessTransformers.add(at.absolutePath) 18 | } 19 | parchment { 20 | minecraftVersion = parchment_minecraft 21 | mappingsVersion = parchment_version 22 | } 23 | } 24 | 25 | dependencies { 26 | compileOnly group: 'org.spongepowered', name: 'mixin', version: '0.8.5' 27 | // fabric and neoforge both bundle mixinextras, so it is safe to use it in common 28 | compileOnly group: 'io.github.llamalad7', name: 'mixinextras-common', version: '0.3.5' 29 | annotationProcessor group: 'io.github.llamalad7', name: 'mixinextras-common', version: '0.3.5' 30 | } 31 | 32 | configurations { 33 | commonJava { 34 | canBeResolved = false 35 | canBeConsumed = true 36 | } 37 | commonResources { 38 | canBeResolved = false 39 | canBeConsumed = true 40 | } 41 | } 42 | 43 | artifacts { 44 | commonJava sourceSets.main.java.sourceDirectories.singleFile 45 | commonResources sourceSets.main.resources.sourceDirectories.singleFile 46 | } 47 | 48 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/ChunkLoadEventListener.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | import java.util.Map; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import net.minecraft.client.Minecraft; 9 | import net.minecraft.world.level.LevelAccessor; 10 | import org.apache.logging.log4j.LogManager; 11 | import org.apache.logging.log4j.Logger; 12 | 13 | public class ChunkLoadEventListener implements Job.Operation { 14 | private static final Logger LOGGER = LogManager.getLogger(); 15 | 16 | interface DoneCallback { 17 | void done(boolean success, boolean removeFromListeners); 18 | } 19 | 20 | // Map packed chunk (x, z) to boolean: true if chunk is loaded, false otherwise. 21 | private final Map chunksToLoad = new ConcurrentHashMap<>(); 22 | 23 | // Level with chunks to listen for. Store hash rather than reference to avoid memory leak. 24 | private final int levelHashCode; 25 | 26 | private final DoneCallback doneCallback; 27 | private int numUnloadedChunks = 0; 28 | private boolean suspended = false; 29 | private boolean finished = false; 30 | 31 | public ChunkLoadEventListener(int x1, int z1, int x2, int z2, DoneCallback doneCallback) { 32 | var minecraft = Minecraft.getInstance(); 33 | this.levelHashCode = minecraft.level.hashCode(); 34 | LOGGER.info("listener chunk region in level {}: {} {} {} {}", levelHashCode, x1, z1, x2, z2); 35 | int chunkX1 = worldCoordToChunkCoord(x1); 36 | int chunkZ1 = worldCoordToChunkCoord(z1); 37 | int chunkX2 = worldCoordToChunkCoord(x2); 38 | int chunkZ2 = worldCoordToChunkCoord(z2); 39 | 40 | int chunkXMin = Math.min(chunkX1, chunkX2); 41 | int chunkXMax = Math.max(chunkX1, chunkX2); 42 | int chunkZMin = Math.min(chunkZ1, chunkZ2); 43 | int chunkZMax = Math.max(chunkZ1, chunkZ2); 44 | 45 | for (int chunkX = chunkXMin; chunkX <= chunkXMax; chunkX++) { 46 | for (int chunkZ = chunkZMin; chunkZ <= chunkZMax; chunkZ++) { 47 | LOGGER.info("listener chunk registered: {} {}", chunkX, chunkZ); 48 | long packedChunkXZ = packInts(chunkX, chunkZ); 49 | chunksToLoad.put(packedChunkXZ, false); 50 | } 51 | } 52 | this.doneCallback = doneCallback; 53 | } 54 | 55 | @Override 56 | public String name() { 57 | return "chunk_load_listener"; 58 | } 59 | 60 | @Override 61 | public synchronized void suspend() { 62 | suspended = true; 63 | } 64 | 65 | /** Resume this listener and return true if it's done. */ 66 | @Override 67 | public synchronized boolean resumeAndCheckDone() { 68 | suspended = false; 69 | 70 | updateChunkStatuses(); 71 | if (checkFullyLoaded()) { 72 | return true; 73 | } 74 | return false; 75 | } 76 | 77 | @Override 78 | public synchronized void cancel() { 79 | onFinished(/*success=*/ false, /*removeFromListeners=*/ true); 80 | } 81 | 82 | public synchronized void updateChunkStatuses() { 83 | var minecraft = Minecraft.getInstance(); 84 | var level = minecraft.level; 85 | if (level.hashCode() != this.levelHashCode) { 86 | LOGGER.info("chunk listener's world doesn't match current world; clearing listener"); 87 | chunksToLoad.clear(); 88 | numUnloadedChunks = 0; 89 | return; 90 | } 91 | numUnloadedChunks = 0; 92 | var chunkManager = level.getChunkSource(); 93 | for (var entry : chunksToLoad.entrySet()) { 94 | long packedChunkXZ = entry.getKey(); 95 | int[] chunkCoords = unpackLong(packedChunkXZ); 96 | boolean isLoaded = chunkManager.getChunkNow(chunkCoords[0], chunkCoords[1]) != null; 97 | entry.setValue(isLoaded); 98 | if (!isLoaded) { 99 | numUnloadedChunks++; 100 | } 101 | } 102 | LOGGER.info("Unloaded chunks after updateChunkStatuses: {}", numUnloadedChunks); 103 | } 104 | 105 | /** Returns true if the final outstanding chunk is loaded. */ 106 | public synchronized boolean onChunkLoaded(LevelAccessor chunkLevel, int chunkX, int chunkZ) { 107 | if (suspended) { 108 | return false; 109 | } 110 | if (chunkLevel.hashCode() != levelHashCode) { 111 | return false; 112 | } 113 | long packedChunkXZ = packInts(chunkX, chunkZ); 114 | if (!chunksToLoad.containsKey(packedChunkXZ)) { 115 | return false; 116 | } 117 | boolean wasLoaded = chunksToLoad.put(packedChunkXZ, true); 118 | if (!wasLoaded) { 119 | LOGGER.info("listener chunk loaded for level {}: {} {}", levelHashCode, chunkX, chunkZ); 120 | numUnloadedChunks--; 121 | if (numUnloadedChunks == 0) { 122 | onFinished(/*success=*/ true, /*removeFromListeners=*/ false); 123 | return true; 124 | } 125 | } 126 | return false; 127 | } 128 | 129 | public synchronized void onChunkUnloaded(LevelAccessor chunkLevel, int chunkX, int chunkZ) { 130 | if (suspended) { 131 | return; 132 | } 133 | if (chunkLevel.hashCode() != levelHashCode) { 134 | return; 135 | } 136 | long packedChunkXZ = packInts(chunkX, chunkZ); 137 | if (!chunksToLoad.containsKey(packedChunkXZ)) { 138 | return; 139 | } 140 | boolean wasLoaded = chunksToLoad.put(packedChunkXZ, false); 141 | if (wasLoaded) { 142 | numUnloadedChunks++; 143 | } 144 | } 145 | 146 | public synchronized boolean checkFullyLoaded() { 147 | if (numUnloadedChunks == 0) { 148 | onFinished(/*success=*/ true, /*removeFromListeners=*/ true); 149 | return true; 150 | } else { 151 | return false; 152 | } 153 | } 154 | 155 | /** To be called when either all requested chunks are loaded or operation is cancelled. */ 156 | private synchronized void onFinished(boolean success, boolean removeFromListeners) { 157 | if (finished) { 158 | LOGGER.warn( 159 | "ChunkLoadEventListener already finished; finished again with {}", 160 | success ? "success" : "failure"); 161 | return; 162 | } 163 | finished = true; 164 | doneCallback.done(success, removeFromListeners); 165 | } 166 | 167 | private static int worldCoordToChunkCoord(int x) { 168 | return (x >= 0) ? (x / 16) : (((x + 1) / 16) - 1); 169 | } 170 | 171 | private static long packInts(int x, int z) { 172 | return (((long) x) << 32) | (z & 0xffffffffL); 173 | } 174 | 175 | /** Unpack 64-bit long into two 32-bit ints written to returned 2-element int array. */ 176 | private static int[] unpackLong(long x) { 177 | return new int[] {(int) (x >> 32), (int) x}; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/CommandSyntax.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | public class CommandSyntax { 11 | 12 | private enum ParseState { 13 | START, 14 | WORD_OUTSIDE_QUOTES, 15 | SPACES_OUTSIDE_QUOTES, 16 | INSIDE_SINGLE_QUOTES, 17 | INSIDE_DOUBLE_QUOTES 18 | } 19 | 20 | public static String quoteString(String value) { 21 | return quoteString(value, false); 22 | } 23 | 24 | public static String quoteString(String value, boolean alwaysQuote) { 25 | if (value.isEmpty()) { 26 | return "\"\""; 27 | } 28 | 29 | long numWhitespace = value.chars().filter(ch -> ch == ' ' || ch == '\n').count(); 30 | long numBackslashes = value.chars().filter(ch -> ch == '\\').count(); 31 | long numSingleQuotes = value.chars().filter(ch -> ch == '\'').count(); 32 | long numDoubleQuotes = value.chars().filter(ch -> ch == '"').count(); 33 | 34 | if (numWhitespace == 0 && numBackslashes == 0 && numSingleQuotes == 0 && numDoubleQuotes == 0) { 35 | if (alwaysQuote) { 36 | return '"' + value + '"'; 37 | } else { 38 | return value; 39 | } 40 | } 41 | 42 | var buffer = new StringBuilder(); 43 | if (numDoubleQuotes > numSingleQuotes) { 44 | buffer.append('\''); 45 | buffer.append(value.replace("\\", "\\\\").replace("\n", "\\n").replace("'", "\\'")); 46 | buffer.append('\''); 47 | } else { 48 | buffer.append('"'); 49 | buffer.append(value.replace("\\", "\\\\").replace("\n", "\\n").replace("\"", "\\\"")); 50 | buffer.append('"'); 51 | } 52 | return buffer.toString(); 53 | } 54 | 55 | public static String quoteCommand(String[] command) { 56 | var buffer = new StringBuilder(); 57 | for (String value : command) { 58 | if (buffer.length() == 0) { 59 | // Special-case the first element since it's the command and we don't want to escape the 60 | // leading backslash. 61 | buffer.append(value); 62 | } else { 63 | buffer.append(' '); 64 | buffer.append(quoteString(value)); 65 | } 66 | } 67 | return buffer.toString(); 68 | } 69 | 70 | public static class Token { 71 | 72 | public static enum Type { 73 | STRING, 74 | AND, 75 | OR, 76 | SEMICOLON, 77 | REDIRECT_STDOUT, 78 | REDIRECT_STDERR 79 | } 80 | 81 | private static final Token AND_TOKEN = new Token(Type.AND); 82 | private static final Token OR_TOKEN = new Token(Type.OR); 83 | private static final Token SEMICOLON_TOKEN = new Token(Type.SEMICOLON); 84 | private static final Token REDIRECT_STDOUT_TOKEN = new Token(Type.REDIRECT_STDOUT); 85 | private static final Token REDIRECT_STDERR_TOKEN = new Token(Type.REDIRECT_STDERR); 86 | 87 | private final Optional string; 88 | private final Type type; 89 | 90 | public static Token string(String string) { 91 | return new Token(string); 92 | } 93 | 94 | public static Token and() { 95 | return AND_TOKEN; 96 | } 97 | 98 | public static Token or() { 99 | return OR_TOKEN; 100 | } 101 | 102 | public static Token semicolon() { 103 | return SEMICOLON_TOKEN; 104 | } 105 | 106 | public static Token redirectStdout() { 107 | return REDIRECT_STDOUT_TOKEN; 108 | } 109 | 110 | public static Token redirectStderr() { 111 | return REDIRECT_STDERR_TOKEN; 112 | } 113 | 114 | public Type type() { 115 | return type; 116 | } 117 | 118 | @Override 119 | public String toString() { 120 | switch (type) { 121 | case STRING: 122 | return string.get(); 123 | case AND: 124 | return "&&"; 125 | case OR: 126 | return "||"; 127 | case SEMICOLON: 128 | return ";"; 129 | case REDIRECT_STDOUT: 130 | return ">"; 131 | case REDIRECT_STDERR: 132 | return "2>"; 133 | default: 134 | throw new IllegalStateException("Unsupported Token type: `" + type.toString() + "`"); 135 | } 136 | } 137 | 138 | private Token(String string) { 139 | this.type = Type.STRING; 140 | this.string = Optional.of(string); 141 | } 142 | 143 | private Token(Type type) { 144 | this.type = type; 145 | this.string = Optional.empty(); 146 | } 147 | } 148 | 149 | private static String consumeStringBuilder(StringBuilder builder) { 150 | String s = builder.toString(); 151 | builder.setLength(0); 152 | return s; 153 | } 154 | 155 | public static List parseCommand(String command) { 156 | command += " "; // add space for simpler termination of parsing 157 | List args = new ArrayList<>(); 158 | var state = ParseState.START; 159 | var argBuilder = new StringBuilder(); 160 | var literalArgBuilder = new StringBuilder(); // buffer for arg preserving literals like quotes 161 | char prevCh = '\0'; 162 | for (int i = 0; i < command.length(); ++i) { 163 | char ch = command.charAt(i); 164 | switch (state) { 165 | case START: 166 | switch (ch) { 167 | case '\'': 168 | state = ParseState.INSIDE_SINGLE_QUOTES; 169 | break; 170 | case '"': 171 | state = ParseState.INSIDE_DOUBLE_QUOTES; 172 | break; 173 | case ' ': 174 | state = ParseState.SPACES_OUTSIDE_QUOTES; 175 | break; 176 | default: 177 | state = ParseState.WORD_OUTSIDE_QUOTES; 178 | argBuilder.append(ch); 179 | } 180 | literalArgBuilder.append(ch); 181 | break; 182 | case WORD_OUTSIDE_QUOTES: 183 | switch (ch) { 184 | case '\'': 185 | if (prevCh == '\\') { 186 | argBuilder.setLength(argBuilder.length() - 1); 187 | argBuilder.append(ch); 188 | } else { 189 | state = ParseState.INSIDE_SINGLE_QUOTES; 190 | } 191 | literalArgBuilder.append(ch); 192 | break; 193 | case '"': 194 | if (prevCh == '\\') { 195 | argBuilder.setLength(argBuilder.length() - 1); 196 | argBuilder.append(ch); 197 | } else { 198 | state = ParseState.INSIDE_DOUBLE_QUOTES; 199 | } 200 | literalArgBuilder.append(ch); 201 | break; 202 | case ' ': 203 | { 204 | String arg = consumeStringBuilder(argBuilder); 205 | String literalArg = consumeStringBuilder(literalArgBuilder); 206 | if (literalArg.equals("&&")) { 207 | args.add(Token.and()); 208 | } else if (literalArg.equals("||")) { 209 | args.add(Token.or()); 210 | } else if (literalArg.endsWith(";")) { 211 | String argPrefix = arg.substring(0, arg.length() - 1); 212 | if (!argPrefix.isEmpty()) { 213 | args.add(Token.string(argPrefix)); 214 | } 215 | args.add(Token.semicolon()); 216 | } else if (literalArg.startsWith(">")) { 217 | args.add(Token.redirectStdout()); 218 | String argSuffix = arg.substring(1); 219 | if (!argSuffix.isEmpty()) { 220 | args.add(Token.string(argSuffix)); 221 | } 222 | } else if (literalArg.startsWith("2>")) { 223 | args.add(Token.redirectStderr()); 224 | String argSuffix = arg.substring(2); 225 | if (!argSuffix.isEmpty()) { 226 | args.add(Token.string(argSuffix)); 227 | } 228 | } else { 229 | args.add(Token.string(arg)); 230 | } 231 | state = ParseState.SPACES_OUTSIDE_QUOTES; 232 | break; 233 | } 234 | default: 235 | argBuilder.append(ch); 236 | literalArgBuilder.append(ch); 237 | } 238 | break; 239 | case SPACES_OUTSIDE_QUOTES: 240 | switch (ch) { 241 | case '\'': 242 | state = ParseState.INSIDE_SINGLE_QUOTES; 243 | literalArgBuilder.append(ch); 244 | break; 245 | case '"': 246 | state = ParseState.INSIDE_DOUBLE_QUOTES; 247 | literalArgBuilder.append(ch); 248 | break; 249 | case ' ': 250 | break; 251 | default: 252 | argBuilder.append(ch); 253 | literalArgBuilder.append(ch); 254 | state = ParseState.WORD_OUTSIDE_QUOTES; 255 | } 256 | break; 257 | case INSIDE_SINGLE_QUOTES: 258 | switch (ch) { 259 | case '\'': 260 | if (prevCh == '\\') { 261 | argBuilder.setLength(argBuilder.length() - 1); 262 | argBuilder.append(ch); 263 | } else { 264 | state = ParseState.WORD_OUTSIDE_QUOTES; 265 | } 266 | break; 267 | case 'n': 268 | if (prevCh == '\\') { 269 | argBuilder.setLength(argBuilder.length() - 1); 270 | argBuilder.append('\n'); 271 | break; 272 | } 273 | // intentional fallthru 274 | default: 275 | argBuilder.append(ch); 276 | } 277 | literalArgBuilder.append(ch); 278 | break; 279 | case INSIDE_DOUBLE_QUOTES: 280 | switch (ch) { 281 | case '"': 282 | if (prevCh == '\\') { 283 | argBuilder.setLength(argBuilder.length() - 1); 284 | argBuilder.append(ch); 285 | } else { 286 | state = ParseState.WORD_OUTSIDE_QUOTES; 287 | } 288 | break; 289 | case 'n': 290 | if (prevCh == '\\') { 291 | argBuilder.setLength(argBuilder.length() - 1); 292 | argBuilder.append('\n'); 293 | break; 294 | } 295 | // intentional fallthru 296 | default: 297 | argBuilder.append(ch); 298 | } 299 | literalArgBuilder.append(ch); 300 | break; 301 | } 302 | prevCh = ch; 303 | } 304 | if (argBuilder.length() > 0) { 305 | throw new IllegalStateException( 306 | "Unexpected trailing characters when parsing command `" 307 | + command.substring(0, command.length() - 1) // trailing space added above 308 | + "`: `" 309 | + argBuilder.toString() 310 | + "`"); 311 | } 312 | return args; 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/EntityExporter.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | import com.google.gson.JsonArray; 7 | import com.google.gson.JsonObject; 8 | import java.util.HashMap; 9 | import java.util.HashSet; 10 | import java.util.Map; 11 | import java.util.Set; 12 | import net.minecraft.client.Minecraft; 13 | import net.minecraft.nbt.CompoundTag; 14 | import net.minecraft.world.entity.Entity; 15 | import net.minecraft.world.entity.LivingEntity; 16 | import org.apache.logging.log4j.LogManager; 17 | import org.apache.logging.log4j.Logger; 18 | 19 | /** Utility for exporting entities as JSON. */ 20 | public class EntityExporter { 21 | private static final Logger LOGGER = LogManager.getLogger(); 22 | 23 | private final double positionInterpolation; 24 | private final boolean includeNbt; 25 | 26 | // UUIDs of entities that have already been exported from this EntityExporter. 27 | private final Set exportedEntityUuids = new HashSet<>(); 28 | 29 | // Entities that have been referenced by this EntityExporter but not yet exported. E.g. entities 30 | // referenced as passengers. This allows the exporter to track entities that are referenced as 31 | // passengers but might not be listed as top-level entities. This allows the exporter to ensure 32 | // that all referenced entities get their data exported. 33 | private final Map entitiesToExport = new HashMap<>(); 34 | 35 | private static class DuplicateEntityException extends RuntimeException { 36 | public DuplicateEntityException(String message) { 37 | super(message); 38 | } 39 | } 40 | 41 | /** 42 | * Create {@code EntityExporter}. 43 | * 44 | * @param positionInterpolation value from 0 to 1 indicating time ratio from last tick to next 45 | * @param includeNbt if true, export NBT data for each entity 46 | */ 47 | public EntityExporter(double positionInterpolation, boolean includeNbt) { 48 | this.positionInterpolation = positionInterpolation; 49 | this.includeNbt = includeNbt; 50 | } 51 | 52 | public JsonArray export(Iterable entities) { 53 | var result = entitiesToJsonArray(entities); 54 | clear(); 55 | return result; 56 | } 57 | 58 | public JsonObject export(Entity entity) { 59 | var result = entityToJsonObject(entity); 60 | clear(); 61 | return result; 62 | } 63 | 64 | private void clear() { 65 | exportedEntityUuids.clear(); 66 | entitiesToExport.clear(); 67 | } 68 | 69 | private JsonArray entitiesToJsonArray(Iterable entities) { 70 | var jsonEntities = new JsonArray(); 71 | 72 | // Export top-level entities. 73 | for (var entity : entities) { 74 | try { 75 | jsonEntities.add(entityToJsonObject(entity)); 76 | } catch (DuplicateEntityException e) { 77 | LOGGER.error("Ignoring duplicate entity while exporting to JSON: {}", e.getMessage()); 78 | } 79 | } 80 | 81 | // Export any entities that were referenced by top-level entities but for whatever reason 82 | // weren't listed among top-level entities. This ensures that all referenced entities are 83 | // themselves exported. 84 | for (var entity : entitiesToExport.values()) { 85 | try { 86 | jsonEntities.add(entityToJsonObject(entity)); 87 | } catch (DuplicateEntityException e) { 88 | LOGGER.error("Ignoring duplicate entity while exporting to JSON: {}", e.getMessage()); 89 | } 90 | } 91 | return jsonEntities; 92 | } 93 | 94 | private JsonObject entityToJsonObject(Entity entity) { 95 | String uuid = entity.getUUID().toString(); 96 | if (exportedEntityUuids.contains(uuid)) { 97 | throw new DuplicateEntityException(uuid); 98 | } 99 | exportedEntityUuids.add(uuid); 100 | 101 | if (entitiesToExport.containsKey(uuid)) { 102 | // This entity was previously referenced as a passenger of another entity that was exported. 103 | // Now that the passenger is being exported, remove this entity from entitiesToExport. 104 | entitiesToExport.remove(uuid); 105 | } 106 | 107 | var minecraft = Minecraft.getInstance(); 108 | var jsonEntity = new JsonObject(); 109 | jsonEntity.addProperty("name", entity.getName().getString()); 110 | jsonEntity.addProperty("type", entity.getType().toString()); 111 | jsonEntity.addProperty("uuid", uuid); 112 | jsonEntity.addProperty("id", entity.getId()); 113 | if (entity instanceof LivingEntity livingEntity) { 114 | jsonEntity.addProperty("health", livingEntity.getHealth()); 115 | } 116 | if (entity == minecraft.player) { 117 | jsonEntity.addProperty("local", true); 118 | } 119 | 120 | var v = entity.getDeltaMovement(); 121 | 122 | double x = entity.getX(); 123 | double y = entity.getY(); 124 | double z = entity.getZ(); 125 | 126 | var position = new JsonArray(); 127 | position.add(x); 128 | position.add(y); 129 | position.add(z); 130 | jsonEntity.add("position", position); 131 | 132 | final double epsilon = 0.0001; 133 | if (positionInterpolation > epsilon 134 | && (Math.abs(v.x) > epsilon || Math.abs(v.y) > epsilon || Math.abs(v.z) > epsilon)) { 135 | var lerpPosition = new JsonArray(); 136 | lerpPosition.add(x + v.x * positionInterpolation); 137 | lerpPosition.add(y + v.y * positionInterpolation); 138 | lerpPosition.add(z + v.z * positionInterpolation); 139 | jsonEntity.add("lerp_position", lerpPosition); 140 | } 141 | 142 | jsonEntity.addProperty("yaw", entity.getYRot()); 143 | jsonEntity.addProperty("pitch", entity.getXRot()); 144 | 145 | var velocity = new JsonArray(); 146 | velocity.add(v.x); 147 | velocity.add(v.y); 148 | velocity.add(v.z); 149 | jsonEntity.add("velocity", velocity); 150 | 151 | if (!entity.getPassengers().isEmpty()) { 152 | var jsonPassengers = new JsonArray(); 153 | for (var passenger : entity.getPassengers()) { 154 | jsonPassengers.add(passenger.getUUID().toString()); 155 | } 156 | jsonEntity.add("passengers", jsonPassengers); 157 | } 158 | 159 | if (includeNbt) { 160 | var nbt = new CompoundTag(); 161 | jsonEntity.addProperty("nbt", entity.saveWithoutId(nbt).toString()); 162 | } 163 | 164 | return jsonEntity; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/EntitySelection.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Comparator; 8 | import java.util.List; 9 | import java.util.Optional; 10 | import java.util.OptionalDouble; 11 | import java.util.OptionalInt; 12 | import java.util.regex.Pattern; 13 | import net.minecraft.client.Minecraft; 14 | import net.minecraft.world.entity.Entity; 15 | 16 | public record EntitySelection( 17 | Optional uuid, 18 | Optional name, 19 | Optional type, 20 | Optional> position, 21 | Optional> offset, 22 | OptionalDouble minDistance, 23 | OptionalDouble maxDistance, 24 | Optional sort, 25 | OptionalInt limit) { 26 | 27 | public static enum SortType { 28 | NEAREST, 29 | FURTHEST, 30 | RANDOM, 31 | ARBITRARY 32 | } 33 | 34 | public List selectFrom(Iterable entities) { 35 | var minecraft = Minecraft.getInstance(); 36 | var player = minecraft.player; 37 | 38 | Optional uuidPattern = uuid.map(Pattern::compile); 39 | Optional namePattern = name.map(Pattern::compile); 40 | Optional typePattern = type.map(Pattern::compile); 41 | 42 | OptionalDouble minDistanceSquared = optionalSquare(minDistance); 43 | OptionalDouble maxDistanceSquared = optionalSquare(maxDistance); 44 | 45 | double baseX = player.getX(); 46 | double baseY = player.getY(); 47 | double baseZ = player.getZ(); 48 | if (position.isPresent()) { 49 | baseX = position.get().get(0); 50 | baseY = position.get().get(1); 51 | baseZ = position.get().get(2); 52 | } 53 | 54 | double offsetX = 0.; 55 | double offsetY = 0.; 56 | double offsetZ = 0.; 57 | if (offset.isPresent()) { 58 | offsetX = offset.get().get(0); 59 | offsetY = offset.get().get(1); 60 | offsetZ = offset.get().get(2); 61 | } 62 | 63 | double minX = Math.min(baseX, baseX + offsetX); 64 | double maxX = Math.max(baseX, baseX + offsetX); 65 | double minY = Math.min(baseY, baseY + offsetY); 66 | double maxY = Math.max(baseY, baseY + offsetY); 67 | double minZ = Math.min(baseZ, baseZ + offsetZ); 68 | double maxZ = Math.max(baseZ, baseZ + offsetZ); 69 | 70 | List results = new ArrayList<>(); 71 | for (var entity : entities) { 72 | double entityX = entity.getX(); 73 | double entityY = entity.getY(); 74 | double entityZ = entity.getZ(); 75 | double entityDistance = -1.; // negative means unassigned 76 | 77 | if (minDistanceSquared.isPresent()) { 78 | entityDistance = 79 | Math3d.computeDistanceSquared(baseX, baseY, baseZ, entityX, entityY, entityZ); 80 | if (entityDistance < minDistanceSquared.getAsDouble()) { 81 | continue; 82 | } 83 | } 84 | 85 | if (maxDistanceSquared.isPresent()) { 86 | // Don't recompute entity distance if already computed above. 87 | if (entityDistance < 0) { 88 | entityDistance = 89 | Math3d.computeDistanceSquared(baseX, baseY, baseZ, entityX, entityY, entityZ); 90 | } 91 | if (entityDistance > maxDistanceSquared.getAsDouble()) { 92 | continue; 93 | } 94 | } 95 | 96 | if (offset.isPresent()) { 97 | if (entityX < minX || entityY < minY || entityZ < minZ) { 98 | continue; 99 | } 100 | if (entityX > maxX || entityY > maxY || entityZ > maxZ) { 101 | continue; 102 | } 103 | } 104 | 105 | if (uuidPattern.isPresent()) { 106 | var match = uuidPattern.get().matcher(entity.getUUID().toString()); 107 | if (!match.matches()) { 108 | continue; 109 | } 110 | } 111 | 112 | if (namePattern.isPresent()) { 113 | var match = namePattern.get().matcher(entity.getName().getString()); 114 | if (!match.matches()) { 115 | continue; 116 | } 117 | } 118 | 119 | if (typePattern.isPresent()) { 120 | var match = typePattern.get().matcher(entity.getType().toString()); 121 | if (!match.matches()) { 122 | continue; 123 | } 124 | } 125 | 126 | results.add(entity); 127 | } 128 | 129 | if (sort.isPresent()) { 130 | switch (sort.get()) { 131 | case NEAREST: 132 | results.sort(EntityDistanceComparator.nearest(baseX, baseY, baseZ)); 133 | break; 134 | case FURTHEST: 135 | results.sort(EntityDistanceComparator.furthest(baseX, baseY, baseZ)); 136 | break; 137 | case RANDOM: 138 | results.sort(new EntityHashComparator()); 139 | break; 140 | case ARBITRARY: 141 | break; 142 | } 143 | } 144 | 145 | if (limit.isPresent()) { 146 | int limitInt = limit.getAsInt(); 147 | if (limitInt < results.size()) { 148 | results = results.subList(0, limitInt); 149 | } 150 | } 151 | 152 | return results; 153 | } 154 | 155 | private static class EntityDistanceComparator implements Comparator { 156 | private final double x; 157 | private final double y; 158 | private final double z; 159 | private final int sign; 160 | 161 | public static EntityDistanceComparator nearest(double x, double y, double z) { 162 | return new EntityDistanceComparator(x, y, z, 1); 163 | } 164 | 165 | public static EntityDistanceComparator furthest(double x, double y, double z) { 166 | return new EntityDistanceComparator(x, y, z, -1); 167 | } 168 | 169 | private EntityDistanceComparator(double x, double y, double z, int sign) { 170 | this.x = x; 171 | this.y = y; 172 | this.z = z; 173 | this.sign = sign; 174 | } 175 | 176 | @Override 177 | public int compare(Entity e1, Entity e2) { 178 | if (e1 == e2) { 179 | return 0; 180 | } 181 | int comparison = 182 | Double.compare( 183 | computeEntityDistanceSquared(x, y, z, e1), computeEntityDistanceSquared(x, y, z, e2)); 184 | if (comparison == 0) { 185 | return Integer.compare(e1.hashCode(), e2.hashCode()); 186 | } 187 | return sign * comparison; 188 | } 189 | } 190 | 191 | private static class EntityHashComparator implements Comparator { 192 | @Override 193 | public int compare(Entity e1, Entity e2) { 194 | if (e1 == e2) { 195 | return 0; 196 | } 197 | int comparison = Integer.compare(e1.hashCode(), e2.hashCode()); 198 | if (comparison == 0) { 199 | return Double.compare(e1.getX(), e2.getX()); 200 | } 201 | return comparison; 202 | } 203 | } 204 | 205 | private static double computeEntityDistanceSquared(double x, double y, double z, Entity e) { 206 | double x2 = e.getX(); 207 | double y2 = e.getY(); 208 | double z2 = e.getZ(); 209 | 210 | double dx = x - x2; 211 | double dy = y - y2; 212 | double dz = z - z2; 213 | return dx * dx + dy * dy + dz * dz; 214 | } 215 | 216 | private static OptionalDouble optionalSquare(OptionalDouble od) { 217 | if (od.isEmpty()) { 218 | return od; 219 | } else { 220 | double d = od.getAsDouble(); 221 | return OptionalDouble.of(d * d); 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/ExceptionInfo.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | import com.google.common.collect.ImmutableList; 7 | import java.util.List; 8 | 9 | public record ExceptionInfo(String type, String message, String desc, List stack) { 10 | 11 | public record StackElement(String file, String method, int line) {} 12 | 13 | public static ExceptionInfo fromException(Exception e) { 14 | var type = e.getClass().getName(); 15 | var desc = e.toString(); 16 | var stackBuilder = new ImmutableList.Builder(); 17 | boolean hitMinescriptJava = false; 18 | int stackDepth = 0; 19 | for (var element : e.getStackTrace()) { 20 | if (++stackDepth > 10) { 21 | break; 22 | } 23 | var className = element.getClassName(); 24 | var fileName = element.getFileName(); 25 | if (className != null) { 26 | // Capture stacktrace up through Minescript code, but no further. 27 | if (!hitMinescriptJava && className.contains("minescript")) { 28 | hitMinescriptJava = true; 29 | } else if (hitMinescriptJava && !className.contains("minescript")) { 30 | break; 31 | } 32 | } 33 | stackBuilder.add( 34 | new StackElement(fileName, element.getMethodName(), element.getLineNumber())); 35 | } 36 | return new ExceptionInfo(type, e.getMessage(), desc, stackBuilder.build()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/FunctionExecutor.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | public enum FunctionExecutor { 7 | TICK_LOOP("T"), // Process script functions 20 times per second with game ticks. 8 | RENDER_LOOP("R"), // Process script functions at the rendering frame rate, about 30-100 fps. 9 | SCRIPT_LOOP("S"); // Process script functions on the subprocess IO thread, >1000x per second. 10 | 11 | private final String value; 12 | 13 | FunctionExecutor(String value) { 14 | this.value = value; 15 | } 16 | 17 | public String value() { 18 | return value; 19 | } 20 | 21 | public static FunctionExecutor fromValue(String value) { 22 | switch (value) { 23 | case "T": 24 | return TICK_LOOP; 25 | case "R": 26 | return RENDER_LOOP; 27 | case "S": 28 | return SCRIPT_LOOP; 29 | default: 30 | throw new IllegalArgumentException("No FunctionExecutor for value: " + value); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/JobControl.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | import com.google.gson.JsonElement; 7 | import java.util.Queue; 8 | import org.apache.logging.log4j.message.ParameterizedMessage; 9 | 10 | public interface JobControl { 11 | int jobId(); 12 | 13 | JobState state(); 14 | 15 | void yield(); 16 | 17 | Queue renderQueue(); 18 | 19 | Queue tickQueue(); 20 | 21 | boolean respond(long functionCallId, JsonElement returnValue, boolean finalReply); 22 | 23 | boolean raiseException(long functionCallId, ExceptionInfo exception); 24 | 25 | void processStdout(String text); 26 | 27 | void processStderr(String text); 28 | 29 | default void log(String messagePattern, Object... arguments) { 30 | String logMessage = 31 | (arguments.length == 0) 32 | ? messagePattern 33 | : ParameterizedMessage.format(messagePattern, arguments); 34 | processStderr(logMessage); 35 | } 36 | 37 | void logJobException(Exception e); 38 | } 39 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/JobState.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | public enum JobState { 7 | NOT_STARTED("Not started"), 8 | RUNNING("Running"), 9 | SUSPENDED("Suspended"), 10 | KILLED("Killed"), 11 | DONE("Done"); 12 | 13 | private final String displayName; 14 | 15 | private JobState(String displayName) { 16 | this.displayName = displayName; 17 | } 18 | 19 | @Override 20 | public String toString() { 21 | return displayName; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/Math3d.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | public class Math3d { 7 | public static double computeDistanceSquared( 8 | double x1, double y1, double z1, double x2, double y2, double z2) { 9 | double dx = x1 - x2; 10 | double dy = y1 - y2; 11 | double dz = z1 - z2; 12 | return dx * dx + dy * dy + dz * dz; 13 | } 14 | 15 | public static double computeDistance( 16 | double x1, double y1, double z1, double x2, double y2, double z2) { 17 | return Math.sqrt(computeDistanceSquared(x1, y1, z1, x2, y2, z2)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/Message.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | import java.util.List; 7 | 8 | public record Message(Message.Type type, String value, Record data) { 9 | public enum Type { 10 | FUNCTION_CALL, 11 | MINECRAFT_COMMAND, 12 | MINESCRIPT_COMMAND, 13 | CHAT_MESSAGE, 14 | PLAIN_TEXT, 15 | JSON_FORMATTED_TEXT 16 | } 17 | 18 | public record FunctionCallData( 19 | long funcCallId, FunctionExecutor functionExecutor, List args) {} 20 | 21 | public Message(Message.Type type, String value) { 22 | this(type, value, null); 23 | } 24 | 25 | public static Message createFunctionCall( 26 | long funcCallId, FunctionExecutor functionExecutor, String functionName, List args) { 27 | return new Message( 28 | Type.FUNCTION_CALL, functionName, new FunctionCallData(funcCallId, functionExecutor, args)); 29 | } 30 | 31 | public static Message createMinecraftCommand(String value) { 32 | return new Message(Type.MINECRAFT_COMMAND, value); 33 | } 34 | 35 | public static Message createMinescriptCommand(String value) { 36 | return new Message(Type.MINESCRIPT_COMMAND, value); 37 | } 38 | 39 | public static Message createChatMessage(String value) { 40 | return new Message(Type.CHAT_MESSAGE, value); 41 | } 42 | 43 | public static Message fromPlainText(String value) { 44 | return new Message(Type.PLAIN_TEXT, value); 45 | } 46 | 47 | public static Message fromJsonFormattedText(String value) { 48 | return new Message(Type.JSON_FORMATTED_TEXT, value); 49 | } 50 | 51 | public static Message formatAsJsonColoredText(String text, String color) { 52 | return Message.fromJsonFormattedText( 53 | "{\"text\":\"" 54 | + text.replace("\\", "\\\\").replace("\"", "\\\"") 55 | + "\",\"color\":\"" 56 | + color 57 | + "\"}"); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/Numbers.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | public class Numbers { 7 | private Numbers() {} 8 | 9 | public static Number add(Number x, Number y) { 10 | if (x instanceof Double d) { 11 | return d + y.doubleValue(); 12 | } else if (y instanceof Double d) { 13 | return x.doubleValue() + d; 14 | } else if (x instanceof Float f) { 15 | return f + y.floatValue(); 16 | } else if (y instanceof Float f) { 17 | return x.floatValue() + f; 18 | } else if (x instanceof Long l) { 19 | return l + y.longValue(); 20 | } else if (y instanceof Long l) { 21 | return x.longValue() + l; 22 | } else if (x instanceof Integer i) { 23 | return i + y.intValue(); 24 | } else if (y instanceof Integer i) { 25 | return x.intValue() + i; 26 | } else if (x instanceof Short s) { 27 | return s + y.shortValue(); 28 | } else if (y instanceof Short s) { 29 | return x.shortValue() + s; 30 | } else if (x instanceof Byte b) { 31 | return b + y.byteValue(); 32 | } else if (y instanceof Byte b) { 33 | return x.byteValue() + b; 34 | } else { 35 | throw new IllegalArgumentException( 36 | String.format( 37 | "Unable to add numbers: %s + %s (%s + %s)", 38 | x, y, x.getClass().getName(), y.getClass().getName())); 39 | } 40 | } 41 | 42 | public static Number subtract(Number x, Number y) { 43 | if (x instanceof Double d) { 44 | return d - y.doubleValue(); 45 | } else if (y instanceof Double d) { 46 | return x.doubleValue() - d; 47 | } else if (x instanceof Float f) { 48 | return f - y.floatValue(); 49 | } else if (y instanceof Float f) { 50 | return x.floatValue() - f; 51 | } else if (x instanceof Long l) { 52 | return l - y.longValue(); 53 | } else if (y instanceof Long l) { 54 | return x.longValue() - l; 55 | } else if (x instanceof Integer i) { 56 | return i - y.intValue(); 57 | } else if (y instanceof Integer i) { 58 | return x.intValue() - i; 59 | } else if (x instanceof Short s) { 60 | return s - y.shortValue(); 61 | } else if (y instanceof Short s) { 62 | return x.shortValue() - s; 63 | } else if (x instanceof Byte b) { 64 | return b - y.byteValue(); 65 | } else if (y instanceof Byte b) { 66 | return x.byteValue() - b; 67 | } else { 68 | throw new IllegalArgumentException( 69 | String.format( 70 | "Unable to subtract numbers: %s - %s (%s - %s)", 71 | x, y, x.getClass().getName(), y.getClass().getName())); 72 | } 73 | } 74 | 75 | public static Number multiply(Number x, Number y) { 76 | if (x instanceof Double d) { 77 | return d * y.doubleValue(); 78 | } else if (y instanceof Double d) { 79 | return x.doubleValue() * d; 80 | } else if (x instanceof Float f) { 81 | return f * y.floatValue(); 82 | } else if (y instanceof Float f) { 83 | return x.floatValue() * f; 84 | } else if (x instanceof Long l) { 85 | return l * y.longValue(); 86 | } else if (y instanceof Long l) { 87 | return x.longValue() * l; 88 | } else if (x instanceof Integer i) { 89 | return i * y.intValue(); 90 | } else if (y instanceof Integer i) { 91 | return x.intValue() * i; 92 | } else if (x instanceof Short s) { 93 | return s * y.shortValue(); 94 | } else if (y instanceof Short s) { 95 | return x.shortValue() * s; 96 | } else if (x instanceof Byte b) { 97 | return b * y.byteValue(); 98 | } else if (y instanceof Byte b) { 99 | return x.byteValue() * b; 100 | } else { 101 | throw new IllegalArgumentException( 102 | String.format( 103 | "Unable to multiply numbers: %s * %s (%s * %s)", 104 | x, y, x.getClass().getName(), y.getClass().getName())); 105 | } 106 | } 107 | 108 | public static Number divide(Number x, Number y) { 109 | if (x instanceof Double d) { 110 | return d / y.doubleValue(); 111 | } else if (y instanceof Double d) { 112 | return x.doubleValue() / d; 113 | } else if (x instanceof Float f) { 114 | return f / y.floatValue(); 115 | } else if (y instanceof Float f) { 116 | return x.floatValue() / f; 117 | } else if (x instanceof Long l) { 118 | return l / y.longValue(); 119 | } else if (y instanceof Long l) { 120 | return x.longValue() / l; 121 | } else if (x instanceof Integer i) { 122 | return i / y.intValue(); 123 | } else if (y instanceof Integer i) { 124 | return x.intValue() / i; 125 | } else if (x instanceof Short s) { 126 | return s / y.shortValue(); 127 | } else if (y instanceof Short s) { 128 | return x.shortValue() / s; 129 | } else if (x instanceof Byte b) { 130 | return b / y.byteValue(); 131 | } else if (y instanceof Byte b) { 132 | return x.byteValue() / b; 133 | } else { 134 | throw new IllegalArgumentException( 135 | String.format( 136 | "Unable to divide numbers: %s / %s (%s / %s)", 137 | x, y, x.getClass().getName(), y.getClass().getName())); 138 | } 139 | } 140 | 141 | public static Number negate(Number x) { 142 | if (x instanceof Double d) { 143 | return -d; 144 | } else if (x instanceof Float f) { 145 | return -f; 146 | } else if (x instanceof Long l) { 147 | return -l; 148 | } else if (x instanceof Integer i) { 149 | return -i; 150 | } else if (x instanceof Short s) { 151 | return -s; 152 | } else if (x instanceof Byte b) { 153 | return -b; 154 | } else { 155 | throw new IllegalArgumentException( 156 | String.format("Unable to negate number: %s (%s)", x, x.getClass().getName())); 157 | } 158 | } 159 | 160 | public static boolean lessThan(Number x, Number y) { 161 | if (x instanceof Double d) { 162 | return d < y.doubleValue(); 163 | } else if (y instanceof Double d) { 164 | return x.doubleValue() < d; 165 | } else if (x instanceof Float f) { 166 | return f < y.floatValue(); 167 | } else if (y instanceof Float f) { 168 | return x.floatValue() < f; 169 | } else if (x instanceof Long l) { 170 | return l < y.longValue(); 171 | } else if (y instanceof Long l) { 172 | return x.longValue() < l; 173 | } else if (x instanceof Integer i) { 174 | return i < y.intValue(); 175 | } else if (y instanceof Integer i) { 176 | return x.intValue() < i; 177 | } else if (x instanceof Short s) { 178 | return s < y.shortValue(); 179 | } else if (y instanceof Short s) { 180 | return x.shortValue() < s; 181 | } else if (x instanceof Byte b) { 182 | return b < y.byteValue(); 183 | } else if (y instanceof Byte b) { 184 | return x.byteValue() < b; 185 | } else { 186 | throw new IllegalArgumentException( 187 | String.format( 188 | "Unable to compare numbers: %s < %s (%s < %s)", 189 | x, y, x.getClass().getName(), y.getClass().getName())); 190 | } 191 | } 192 | 193 | public static boolean lessThanOrEquals(Number x, Number y) { 194 | if (x instanceof Double d) { 195 | return d <= y.doubleValue(); 196 | } else if (y instanceof Double d) { 197 | return x.doubleValue() <= d; 198 | } else if (x instanceof Float f) { 199 | return f <= y.floatValue(); 200 | } else if (y instanceof Float f) { 201 | return x.floatValue() <= f; 202 | } else if (x instanceof Long l) { 203 | return l <= y.longValue(); 204 | } else if (y instanceof Long l) { 205 | return x.longValue() <= l; 206 | } else if (x instanceof Integer i) { 207 | return i <= y.intValue(); 208 | } else if (y instanceof Integer i) { 209 | return x.intValue() <= i; 210 | } else if (x instanceof Short s) { 211 | return s <= y.shortValue(); 212 | } else if (y instanceof Short s) { 213 | return x.shortValue() <= s; 214 | } else if (x instanceof Byte b) { 215 | return b <= y.byteValue(); 216 | } else if (y instanceof Byte b) { 217 | return x.byteValue() <= b; 218 | } else { 219 | throw new IllegalArgumentException( 220 | String.format( 221 | "Unable to compare numbers: %s <= %s (%s <= %s)", 222 | x, y, x.getClass().getName(), y.getClass().getName())); 223 | } 224 | } 225 | 226 | public static boolean equals(Number x, Number y) { 227 | if (x instanceof Double d) { 228 | return d == y.doubleValue(); 229 | } else if (y instanceof Double d) { 230 | return x.doubleValue() == d; 231 | } else if (x instanceof Float f) { 232 | return f == y.floatValue(); 233 | } else if (y instanceof Float f) { 234 | return x.floatValue() == f; 235 | } else if (x instanceof Long l) { 236 | return l == y.longValue(); 237 | } else if (y instanceof Long l) { 238 | return x.longValue() == l; 239 | } else if (x instanceof Integer i) { 240 | return i == y.intValue(); 241 | } else if (y instanceof Integer i) { 242 | return x.intValue() == i; 243 | } else if (x instanceof Short s) { 244 | return s == y.shortValue(); 245 | } else if (y instanceof Short s) { 246 | return x.shortValue() == s; 247 | } else if (x instanceof Byte b) { 248 | return b == y.byteValue(); 249 | } else if (y instanceof Byte b) { 250 | return x.byteValue() == b; 251 | } else { 252 | throw new IllegalArgumentException( 253 | String.format( 254 | "Unable to compare numbers: %s == %s (%s == %s)", 255 | x, y, x.getClass().getName(), y.getClass().getName())); 256 | } 257 | } 258 | 259 | public static boolean greaterThanOrEquals(Number x, Number y) { 260 | if (x instanceof Double d) { 261 | return d >= y.doubleValue(); 262 | } else if (y instanceof Double d) { 263 | return x.doubleValue() >= d; 264 | } else if (x instanceof Float f) { 265 | return f >= y.floatValue(); 266 | } else if (y instanceof Float f) { 267 | return x.floatValue() >= f; 268 | } else if (x instanceof Long l) { 269 | return l >= y.longValue(); 270 | } else if (y instanceof Long l) { 271 | return x.longValue() >= l; 272 | } else if (x instanceof Integer i) { 273 | return i >= y.intValue(); 274 | } else if (y instanceof Integer i) { 275 | return x.intValue() >= i; 276 | } else if (x instanceof Short s) { 277 | return s >= y.shortValue(); 278 | } else if (y instanceof Short s) { 279 | return x.shortValue() >= s; 280 | } else if (x instanceof Byte b) { 281 | return b >= y.byteValue(); 282 | } else if (y instanceof Byte b) { 283 | return x.byteValue() >= b; 284 | } else { 285 | throw new IllegalArgumentException( 286 | String.format( 287 | "Unable to compare numbers: %s >= %s (%s >= %s)", 288 | x, y, x.getClass().getName(), y.getClass().getName())); 289 | } 290 | } 291 | 292 | public static boolean greaterThan(Number x, Number y) { 293 | if (x instanceof Double d) { 294 | return d > y.doubleValue(); 295 | } else if (y instanceof Double d) { 296 | return x.doubleValue() > d; 297 | } else if (x instanceof Float f) { 298 | return f > y.floatValue(); 299 | } else if (y instanceof Float f) { 300 | return x.floatValue() > f; 301 | } else if (x instanceof Long l) { 302 | return l > y.longValue(); 303 | } else if (y instanceof Long l) { 304 | return x.longValue() > l; 305 | } else if (x instanceof Integer i) { 306 | return i > y.intValue(); 307 | } else if (y instanceof Integer i) { 308 | return x.intValue() > i; 309 | } else if (x instanceof Short s) { 310 | return s > y.shortValue(); 311 | } else if (y instanceof Short s) { 312 | return x.shortValue() > s; 313 | } else if (x instanceof Byte b) { 314 | return b > y.byteValue(); 315 | } else if (y instanceof Byte b) { 316 | return x.byteValue() > b; 317 | } else { 318 | throw new IllegalArgumentException( 319 | String.format( 320 | "Unable to compare numbers: %s > %s (%s > %s)", 321 | x, y, x.getClass().getName(), y.getClass().getName())); 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/Platform.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | public interface Platform { 7 | String modLoaderName(); 8 | } 9 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/ResourceTracker.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.concurrent.atomic.AtomicLong; 9 | import org.apache.logging.log4j.LogManager; 10 | import org.apache.logging.log4j.Logger; 11 | 12 | /** Tracker for managing resources accessed by a script job. */ 13 | public class ResourceTracker { 14 | private static final Logger LOGGER = LogManager.getLogger(); 15 | 16 | private final String resourceTypeName; 17 | private final int jobId; 18 | private final Config config; 19 | private final AtomicLong idAllocator = new AtomicLong(0); 20 | private final Map resources = new HashMap<>(); 21 | 22 | public enum IdStatus { 23 | UNALLOCATED(0), 24 | ALLOCATED(1), 25 | RELEASED(2); 26 | 27 | private final int code; 28 | 29 | IdStatus(int code) { 30 | this.code = code; 31 | } 32 | 33 | public int code() { 34 | return code; 35 | } 36 | } 37 | 38 | public ResourceTracker(Class resourceType, int jobId, Config config) { 39 | resourceTypeName = resourceType.getSimpleName(); 40 | this.jobId = jobId; 41 | this.config = config; 42 | } 43 | 44 | // Assumes external synchronization on `this`. 45 | private IdStatus getIdStatus(long id) { 46 | if (id <= 0 || id > idAllocator.get()) { 47 | return IdStatus.UNALLOCATED; 48 | } else if (resources.containsKey(id)) { 49 | return IdStatus.ALLOCATED; 50 | } else { 51 | return IdStatus.RELEASED; 52 | } 53 | } 54 | 55 | public long retain(T resource) { 56 | long id = idAllocator.incrementAndGet(); 57 | synchronized (this) { 58 | resources.put(id, resource); 59 | } 60 | if (config.debugOutput()) { 61 | LOGGER.info("Mapped Job[{}] {}[{}]: {}", jobId, resourceTypeName, id, resource); 62 | } 63 | return id; 64 | } 65 | 66 | public void reassignId(long id, T resource) { 67 | synchronized (this) { 68 | switch (getIdStatus(id)) { 69 | case UNALLOCATED: 70 | throw new IllegalArgumentException( 71 | String.format("No %s allocated yet with ID %s", resourceTypeName, id)); 72 | case RELEASED: 73 | throw new IllegalStateException( 74 | String.format("%s with ID %s already released", resourceTypeName, id)); 75 | case ALLOCATED: 76 | resources.put(id, resource); 77 | } 78 | } 79 | if (config.debugOutput()) { 80 | LOGGER.info("Remapped Job[{}] {}[{}]: {}", jobId, resourceTypeName, id, resource); 81 | } 82 | } 83 | 84 | public T getById(long id) { 85 | // Special-case zero as the ID for Java null reference so that it can be produced from scripts. 86 | // Note that zero as null does not preclude non-zero IDs from mapping to null. 87 | if (id == 0L) { 88 | return null; 89 | } 90 | synchronized (this) { 91 | switch (getIdStatus(id)) { 92 | case UNALLOCATED: 93 | throw new IllegalArgumentException( 94 | String.format("No %s allocated yet with ID %s", resourceTypeName, id)); 95 | case RELEASED: 96 | throw new IllegalStateException( 97 | String.format("%s with ID %s already released", resourceTypeName, id)); 98 | case ALLOCATED: 99 | default: 100 | return resources.get(id); 101 | } 102 | } 103 | } 104 | 105 | public T releaseById(long id) { 106 | T resource; 107 | synchronized (this) { 108 | switch (getIdStatus(id)) { 109 | case UNALLOCATED: 110 | throw new IllegalArgumentException( 111 | String.format("No %s allocated yet with ID %s", resourceTypeName, id)); 112 | case RELEASED: 113 | throw new IllegalStateException( 114 | String.format("%s with ID %s already released", resourceTypeName, id)); 115 | case ALLOCATED: 116 | default: 117 | resource = resources.remove(id); 118 | } 119 | } 120 | if (config.debugOutput()) { 121 | LOGGER.info("Unmapped Job[{}] {}[{}]: {}", jobId, resourceTypeName, id, resource); 122 | } 123 | return resource; 124 | } 125 | 126 | public void releaseAll() { 127 | synchronized (this) { 128 | if (config.debugOutput()) { 129 | for (var entry : resources.entrySet()) { 130 | long id = entry.getKey(); 131 | T resource = entry.getValue(); 132 | LOGGER.info("Unmapped Job[{}] {}[{}]: {}", jobId, resourceTypeName, id, resource); 133 | } 134 | } 135 | resources.clear(); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/ScriptConfig.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | import com.google.common.base.Preconditions; 7 | import com.google.common.collect.ImmutableList; 8 | import com.google.common.collect.ImmutableSet; 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.nio.file.Paths; 14 | import java.util.ArrayList; 15 | import java.util.Collections; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.concurrent.ConcurrentHashMap; 19 | import java.util.function.Supplier; 20 | import java.util.stream.Collectors; 21 | import org.apache.logging.log4j.LogManager; 22 | import org.apache.logging.log4j.Logger; 23 | 24 | public class ScriptConfig { 25 | private static final Logger LOGGER = LogManager.getLogger(); 26 | 27 | private final Path minescriptDir; 28 | private final ImmutableList builtinCommands; 29 | private final ImmutableSet ignoreDirs; 30 | 31 | private boolean escapeCommandDoubleQuotes = System.getProperty("os.name").startsWith("Windows"); 32 | 33 | private ImmutableList commandPath = 34 | ImmutableList.of(Paths.get("system", "exec"), Paths.get("")); 35 | 36 | private String minescriptCommandPathEnvVar; 37 | 38 | private Map fileTypeMap = new ConcurrentHashMap<>(); 39 | private List fileExtensions = new ArrayList<>(); 40 | 41 | public ScriptConfig( 42 | String minescriptDirName, 43 | ImmutableList builtinCommands, 44 | ImmutableSet ignoreDirs) { 45 | this.minescriptDir = Paths.get(System.getProperty("user.dir"), minescriptDirName); 46 | this.builtinCommands = builtinCommands; 47 | this.ignoreDirs = ignoreDirs; 48 | 49 | minescriptCommandPathEnvVar = 50 | "MINESCRIPT_COMMAND_PATH=" 51 | + String.join( 52 | File.pathSeparator, 53 | commandPath.stream() 54 | .map(p -> minescriptDir.resolve(p).toString()) 55 | .collect(Collectors.toList())); 56 | } 57 | 58 | public void setCommandPath(List commandPath) { 59 | this.commandPath = ImmutableList.copyOf(commandPath); 60 | 61 | minescriptCommandPathEnvVar = 62 | "MINESCRIPT_COMMAND_PATH=" 63 | + String.join( 64 | File.pathSeparator, 65 | this.commandPath.stream() 66 | .map(p -> minescriptDir.resolve(p).toString()) 67 | .collect(Collectors.toList())); 68 | } 69 | 70 | public void setEscapeCommandDoubleQuotes(boolean enable) { 71 | this.escapeCommandDoubleQuotes = enable; 72 | } 73 | 74 | public boolean escapeCommandDoubleQuotes() { 75 | return escapeCommandDoubleQuotes; 76 | } 77 | 78 | public ImmutableList commandPath() { 79 | return commandPath; 80 | } 81 | 82 | // Add to `matches` all the files in `commandDir` that match `prefix`. 83 | private void addMatchingFilesInDir( 84 | String prefix, Path prefixPath, Path commandDir, List matches) { 85 | Path resolvedCommandDir = minescriptDir.resolve(commandDir); 86 | Path resolvedDir = resolvedCommandDir; 87 | 88 | // Iterate all but the last component of the prefix path to walk the path within commandDir. 89 | for (int i = 0; i < prefixPath.getNameCount() - 1; ++i) { 90 | resolvedDir = resolvedDir.resolve(prefixPath.getName(i)); 91 | if (resolvedDir == null || !Files.isDirectory(resolvedDir)) { 92 | return; 93 | } 94 | } 95 | 96 | if (resolvedDir == null) { 97 | return; 98 | } 99 | // When prefix ends with File.separator ("\" on Windows, "/" otherwise), the last name 100 | // component of the path is the component before the final separator. Treat the last name 101 | // component as empty instead. 102 | final String prefixFileName; 103 | if (prefix.length() > 1 && prefix.endsWith(File.separator)) { 104 | resolvedDir = resolvedDir.resolve(prefixPath.getFileName()); 105 | if (resolvedDir == null || !Files.isDirectory(resolvedDir)) { 106 | return; 107 | } 108 | prefixFileName = ""; 109 | } else { 110 | Path fileName = prefixPath.getFileName(); 111 | if (fileName == null) { 112 | return; 113 | } 114 | prefixFileName = fileName.toString(); 115 | } 116 | 117 | if (resolvedDir == null || !Files.isDirectory(resolvedDir)) { 118 | return; 119 | } 120 | try (var paths = Files.list(resolvedDir)) { 121 | paths.forEach( 122 | path -> { 123 | String fileName = path.getFileName().toString(); 124 | if (Files.isDirectory(path)) { 125 | if (fileName.startsWith(prefixFileName)) { 126 | try { 127 | String relativeName = resolvedCommandDir.relativize(path).toString(); 128 | if (!(commandDir.toString().isEmpty() && ignoreDirs.contains(relativeName))) { 129 | matches.add(relativeName + File.separator); 130 | } 131 | } catch (IllegalArgumentException e) { 132 | LOGGER.info( 133 | "Dir completion: resolvedCommandDir: {} path: {}", resolvedCommandDir, path); 134 | throw e; 135 | } 136 | } 137 | } else { 138 | if (fileExtensions.contains(getFileExtension(fileName))) { 139 | try { 140 | String scriptName = 141 | removeFileExtension(resolvedCommandDir.relativize(path).toString()); 142 | if (scriptName.startsWith(prefix) && !matches.contains(scriptName)) { 143 | matches.add(scriptName); 144 | } 145 | } catch (IllegalArgumentException e) { 146 | LOGGER.info( 147 | "File completion: resolvedCommandDir: {} path: {}", 148 | resolvedCommandDir, 149 | path); 150 | throw e; 151 | } 152 | } 153 | } 154 | }); 155 | } catch (IOException e) { 156 | LOGGER.error("Error listing files inside dir `{}`: {}", resolvedDir, e); 157 | } 158 | } 159 | 160 | /** 161 | * Returns names of commands and directories accessible from command path that match the prefix. 162 | */ 163 | public List findCommandPrefixMatches(String prefix) { 164 | var matches = new ArrayList(); 165 | 166 | Path prefixPath = Paths.get(prefix); 167 | if (prefixPath.isAbsolute()) { 168 | return new ArrayList<>(); 169 | } 170 | 171 | for (String builtin : builtinCommands) { 172 | if (builtin.startsWith(prefix)) { 173 | matches.add(builtin); 174 | } 175 | } 176 | 177 | for (Path commandDir : commandPath) { 178 | addMatchingFilesInDir(prefix, prefixPath, commandDir, matches); 179 | } 180 | 181 | return matches; 182 | } 183 | 184 | public record CommandConfig(String extension, List command, List environment) {} 185 | 186 | public void configureFileType(CommandConfig commandConfig) { 187 | Preconditions.checkNotNull(commandConfig.extension); 188 | Preconditions.checkArgument( 189 | commandConfig.extension.startsWith("."), 190 | "File extension does not start with dot: \"%s\"", 191 | commandConfig.extension); 192 | 193 | var fileTypeConfig = 194 | new FileTypeConfig( 195 | CommandBuilder.create(commandConfig.command, () -> escapeCommandDoubleQuotes), 196 | commandConfig.environment == null 197 | ? new String[0] 198 | : commandConfig.environment.stream() 199 | .map(s -> s.replace("{minescript_dir}", minescriptDir.toString())) 200 | .toArray(String[]::new)); 201 | if (fileTypeMap.put(commandConfig.extension, fileTypeConfig) == null) { 202 | fileExtensions.add(commandConfig.extension); 203 | } else { 204 | LOGGER.warn( 205 | "Existing file extension configuration is being replaced: \"{}\"", 206 | commandConfig.extension); 207 | } 208 | 209 | LOGGER.info( 210 | "Configured file extension `{}` for commands: {}", commandConfig.extension, commandConfig); 211 | } 212 | 213 | public List supportedFileExtensions() { 214 | return fileExtensions; 215 | } 216 | 217 | public Path resolveCommandPath(String command) { 218 | for (Path commandDir : commandPath) { 219 | for (String fileExtension : fileExtensions) { 220 | Path path = minescriptDir.resolve(commandDir).resolve(command + fileExtension); 221 | if (Files.exists(path)) { 222 | return path; 223 | } 224 | } 225 | } 226 | return null; 227 | } 228 | 229 | public ExecutableCommand getExecutableCommand(BoundCommand boundCommand) { 230 | var fileTypeConfig = fileTypeMap.get(boundCommand.fileExtension()); 231 | if (fileTypeConfig == null) { 232 | return null; 233 | } 234 | 235 | var env = new ArrayList(); 236 | Collections.addAll(env, fileTypeConfig.environment()); 237 | if (!minescriptCommandPathEnvVar.isEmpty()) { 238 | env.add(minescriptCommandPathEnvVar); 239 | } 240 | 241 | return new ExecutableCommand( 242 | fileTypeConfig.commandBuilder.buildExecutableCommand(boundCommand), 243 | env.toArray(String[]::new)); 244 | } 245 | 246 | /** 247 | * Returns file extension including the ".", e.g. ".py" for "foo.py", or "" if no extension found. 248 | */ 249 | private static String getFileExtension(String fileName) { 250 | int lastIndex = fileName.lastIndexOf('.'); 251 | if (lastIndex == -1) { 252 | return ""; // No file extension found 253 | } 254 | return fileName.substring(lastIndex); 255 | } 256 | 257 | private static String removeFileExtension(String fileName) { 258 | int lastIndex = fileName.lastIndexOf('.'); 259 | if (lastIndex == -1) { 260 | return fileName; 261 | } 262 | return fileName.substring(0, lastIndex); 263 | } 264 | 265 | public record BoundCommand(Path scriptPath, String[] command, ScriptRedirect.Pair redirects) { 266 | String fileExtension() { 267 | if (scriptPath == null) { 268 | return null; 269 | } 270 | return getFileExtension(scriptPath.getFileName().toString()); 271 | } 272 | } 273 | 274 | public record ExecutableCommand(String[] command, String[] environment) {} 275 | 276 | public record FileTypeConfig(CommandBuilder commandBuilder, String[] environment) {} 277 | 278 | /** 279 | * Command builder for translating Minescript commands into executable commands. 280 | * 281 | * @param pattern command pattern containing "{command}" and "{args}" placeholders 282 | * @param commandIndex index of "{command}" in pattern 283 | * @param argsIndex index of "{args}" in pattern 284 | */ 285 | public record CommandBuilder( 286 | String[] pattern, 287 | int commandIndex, 288 | int argsIndex, 289 | Supplier escapeCommandDoubleQuotesSupplier) { 290 | 291 | public static CommandBuilder create( 292 | List commandPattern, Supplier escapeCommandDoubleQuotesSupplier) { 293 | int commandIndex = commandPattern.indexOf("{command}"); 294 | Preconditions.checkArgument( 295 | commandIndex >= 0, "{command} not found in pattern: %s", commandPattern); 296 | 297 | int argsIndex = commandPattern.indexOf("{args}"); 298 | Preconditions.checkArgument( 299 | argsIndex >= 0, "{args} not found in pattern: %s", commandPattern); 300 | 301 | return new CommandBuilder( 302 | commandPattern.toArray(String[]::new), 303 | commandIndex, 304 | argsIndex, 305 | escapeCommandDoubleQuotesSupplier); 306 | } 307 | 308 | public String[] buildExecutableCommand(BoundCommand boundCommand) { 309 | var executableCommand = new ArrayList(); 310 | boolean escapeCommandDoubleQuotes = escapeCommandDoubleQuotesSupplier.get(); 311 | String[] command = boundCommand.command(); 312 | for (int i = 0; i < pattern.length; ++i) { 313 | if (i == commandIndex) { 314 | executableCommand.add(boundCommand.scriptPath().toString()); 315 | } else if (i == argsIndex) { 316 | for (int j = 1; j < command.length; ++j) { 317 | String arg = command[j]; 318 | if (escapeCommandDoubleQuotes) { 319 | arg = arg.replace("\"", "\\\""); 320 | } 321 | executableCommand.add(arg); 322 | } 323 | } else { 324 | executableCommand.add(pattern[i]); 325 | } 326 | } 327 | return executableCommand.toArray(String[]::new); 328 | } 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/ScriptFunctionRunner.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | import java.util.List; 7 | 8 | public interface ScriptFunctionRunner { 9 | void run(Job job, String functionName, long funcCallId, List parsedArgs); 10 | } 11 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/ScriptRedirect.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | import java.util.List; 7 | 8 | public enum ScriptRedirect { 9 | DEFAULT, 10 | CHAT, 11 | ECHO, 12 | LOG, 13 | NULL; 14 | 15 | public record Pair(ScriptRedirect stdout, ScriptRedirect stderr) { 16 | public static final Pair DEFAULTS = new Pair(DEFAULT, DEFAULT); 17 | } 18 | 19 | public static Pair parseAndRemoveRedirects(List commandTokens) { 20 | ScriptRedirect stdout = DEFAULT; 21 | ScriptRedirect stderr = DEFAULT; 22 | 23 | // Iterate the last 2 command tokens in reverse order, removing ones that correspond to 24 | // redirects. 25 | for (int i = 0; i < 2; ++i) { 26 | if (commandTokens.size() < 3) { 27 | break; 28 | } 29 | 30 | String secondLastToken = commandTokens.get(commandTokens.size() - 2); 31 | String lastToken = commandTokens.get(commandTokens.size() - 1); 32 | if (secondLastToken.equals(">")) { 33 | stdout = parseValue(lastToken); 34 | if (stdout != DEFAULT) { 35 | commandTokens.remove(commandTokens.size() - 1); 36 | commandTokens.remove(commandTokens.size() - 1); 37 | } 38 | } else if (secondLastToken.equals("2>")) { 39 | stderr = parseValue(lastToken); 40 | if (stderr != DEFAULT) { 41 | commandTokens.remove(commandTokens.size() - 1); 42 | commandTokens.remove(commandTokens.size() - 1); 43 | } 44 | } else { 45 | break; 46 | } 47 | } 48 | 49 | if (stdout == DEFAULT && stderr == DEFAULT) { 50 | return Pair.DEFAULTS; 51 | } else { 52 | return new Pair(stdout, stderr); 53 | } 54 | } 55 | 56 | private static ScriptRedirect parseValue(String value) { 57 | switch (value) { 58 | case "chat": 59 | return CHAT; 60 | case "echo": 61 | return ECHO; 62 | case "log": 63 | return LOG; 64 | case "null": 65 | return NULL; 66 | default: 67 | return DEFAULT; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/SubprocessTask.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | import com.google.gson.Gson; 7 | import com.google.gson.GsonBuilder; 8 | import com.google.gson.JsonElement; 9 | import com.google.gson.JsonObject; 10 | import java.io.BufferedReader; 11 | import java.io.BufferedWriter; 12 | import java.io.IOException; 13 | import java.io.InputStreamReader; 14 | import java.io.OutputStreamWriter; 15 | import java.util.concurrent.TimeUnit; 16 | import org.apache.logging.log4j.LogManager; 17 | import org.apache.logging.log4j.Logger; 18 | 19 | public class SubprocessTask implements Task { 20 | private static final Logger LOGGER = LogManager.getLogger(); 21 | private static final Gson GSON = new GsonBuilder().serializeNulls().create(); 22 | 23 | private final Config config; 24 | private JobControl jobControl; 25 | private Process process; 26 | private BufferedWriter stdinWriter; 27 | 28 | public SubprocessTask(Config config) { 29 | this.config = config; 30 | } 31 | 32 | @Override 33 | public int run(ScriptConfig.BoundCommand command, JobControl jobControl) { 34 | if (this.jobControl != null) { 35 | throw new IllegalStateException("SubprocessTask can be run only once: " + jobControl); 36 | } 37 | 38 | this.jobControl = jobControl; 39 | 40 | var exec = config.scriptConfig().getExecutableCommand(command); 41 | if (exec == null) { 42 | jobControl.log( 43 | "Cannot run \"{}\" because execution is not configured for \"{}\" files.", 44 | command.scriptPath(), 45 | command.fileExtension()); 46 | return -1; 47 | } 48 | 49 | try { 50 | process = Runtime.getRuntime().exec(exec.command(), exec.environment()); 51 | } catch (IOException e) { 52 | jobControl.logJobException(e); 53 | return -2; 54 | } 55 | 56 | stdinWriter = new BufferedWriter(new OutputStreamWriter(process.getOutputStream())); 57 | 58 | var stdoutThread = 59 | new Thread(this::processStdout, Thread.currentThread().getName() + "-stdout"); 60 | stdoutThread.start(); 61 | 62 | var stderrThread = 63 | new Thread(this::processStderr, Thread.currentThread().getName() + "-stderr"); 64 | stderrThread.start(); 65 | 66 | try { 67 | while (!process.waitFor(100, TimeUnit.MILLISECONDS)) { 68 | if (jobControl.state() == JobState.KILLED) { 69 | LOGGER.info("Killing script process for job `{}`", jobControl); 70 | process.destroy(); 71 | stdoutThread.interrupt(); 72 | stderrThread.interrupt(); 73 | return -5; 74 | } 75 | } 76 | } catch (InterruptedException e) { 77 | LOGGER.warn("Task thread interrupted while awaiting subprocess for job `{}`", jobControl); 78 | } 79 | 80 | int result = process.exitValue(); 81 | LOGGER.info("Script process exited with {} for job `{}`", result, jobControl); 82 | return result; 83 | } 84 | 85 | private void processStdout() { 86 | try (var stdoutReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { 87 | String line; 88 | while ((line = stdoutReader.readLine()) != null) { 89 | jobControl.yield(); 90 | jobControl.processStdout(line); 91 | } 92 | } catch (IOException e) { 93 | LOGGER.error( 94 | "IOException while reading subprocess stdout for job `{}`: {}", 95 | jobControl, 96 | e.getMessage()); 97 | } finally { 98 | if (Thread.interrupted()) { 99 | LOGGER.warn("Thread interrupted while reading subprocess stdout for job `{}`", jobControl); 100 | } 101 | } 102 | } 103 | 104 | private void processStderr() { 105 | try (var stderrReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { 106 | String line; 107 | while ((line = stderrReader.readLine()) != null) { 108 | jobControl.yield(); 109 | jobControl.log(line); 110 | } 111 | } catch (IOException e) { 112 | LOGGER.error( 113 | "IOException while reading subprocess stderr for job `{}`: {}", 114 | jobControl, 115 | e.getMessage()); 116 | } finally { 117 | if (Thread.interrupted()) { 118 | LOGGER.warn("Thread interrupted while reading subprocess stderr for job `{}`", jobControl); 119 | } 120 | } 121 | } 122 | 123 | @Override 124 | public boolean sendResponse(long functionCallId, JsonElement returnValue, boolean finalReply) { 125 | if (!canRespond()) { 126 | LOGGER.warn( 127 | "Subprocess unresponsive to response from funcCallId {} for job {}: {}", 128 | functionCallId, 129 | jobControl, 130 | returnValue); 131 | return false; 132 | } 133 | try { 134 | var response = new JsonObject(); 135 | response.addProperty("fcid", functionCallId); 136 | response.add("retval", returnValue); 137 | if (finalReply) { 138 | response.addProperty("conn", "close"); 139 | } 140 | String responseString = GSON.toJson(response); 141 | synchronized (this) { 142 | stdinWriter.write(responseString); 143 | stdinWriter.newLine(); 144 | stdinWriter.flush(); 145 | } 146 | return true; 147 | } catch (IOException e) { 148 | LOGGER.error( 149 | "IOException in SubprocessTask sendResponse for job {}: {}", jobControl, e.getMessage()); 150 | return false; 151 | } 152 | } 153 | 154 | @Override 155 | public boolean sendException(long functionCallId, ExceptionInfo exception) { 156 | if (!canRespond()) { 157 | LOGGER.warn( 158 | "Subprocess unresponsive to exception from funcCallId {} for job {}: {}", 159 | functionCallId, 160 | jobControl, 161 | exception); 162 | return false; 163 | } 164 | try { 165 | var response = new JsonObject(); 166 | response.addProperty("fcid", functionCallId); 167 | response.addProperty("conn", "close"); 168 | var json = GSON.toJsonTree(exception); 169 | LOGGER.warn("Translating Java exception as JSON: {}", json); 170 | response.add("except", json); 171 | 172 | String responseString = GSON.toJson(response); 173 | synchronized (this) { 174 | stdinWriter.write(responseString); 175 | stdinWriter.newLine(); 176 | stdinWriter.flush(); 177 | } 178 | return true; 179 | } catch (IOException e) { 180 | LOGGER.error( 181 | "IOException in SubprocessTask sendException for job {}: {}", jobControl, e.getMessage()); 182 | return false; 183 | } 184 | } 185 | 186 | private boolean canRespond() { 187 | return process != null && process.isAlive() && stdinWriter != null; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/SystemMessageQueue.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | import java.io.PrintWriter; 7 | import java.io.StringWriter; 8 | import java.util.Queue; 9 | import java.util.concurrent.ConcurrentLinkedQueue; 10 | import org.apache.logging.log4j.LogManager; 11 | import org.apache.logging.log4j.Logger; 12 | import org.apache.logging.log4j.message.ParameterizedMessage; 13 | 14 | public class SystemMessageQueue { 15 | private static final Logger LOGGER = LogManager.getLogger(); 16 | 17 | private Queue queue = new ConcurrentLinkedQueue(); 18 | 19 | public void add(Message message) { 20 | queue.add(message); 21 | } 22 | 23 | public boolean isEmpty() { 24 | return queue.isEmpty(); 25 | } 26 | 27 | public Message poll() { 28 | return queue.poll(); 29 | } 30 | 31 | public void clear() { 32 | queue.clear(); 33 | } 34 | 35 | public void logUserInfo(String messagePattern, Object... arguments) { 36 | String logMessage = ParameterizedMessage.format(messagePattern, arguments); 37 | LOGGER.info("{}", logMessage); 38 | queue.add(Message.formatAsJsonColoredText(logMessage, "yellow")); 39 | } 40 | 41 | public void logUserError(String messagePattern, Object... arguments) { 42 | String logMessage = ParameterizedMessage.format(messagePattern, arguments); 43 | LOGGER.error("{}", logMessage); 44 | queue.add(Message.formatAsJsonColoredText(logMessage, "red")); 45 | } 46 | 47 | public void logException(Exception e) { 48 | logException( 49 | e, 50 | String.format( 51 | "Minescript internal error: %s (see logs/latest.log for details; to browse or report" 52 | + " issues see https://minescript.net/issues)", 53 | e.toString())); 54 | } 55 | 56 | public void logException(Exception e, String chatMessage) { 57 | var sw = new StringWriter(); 58 | var pw = new PrintWriter(sw); 59 | e.printStackTrace(pw); 60 | logUserError(chatMessage); 61 | LOGGER.error(sw.toString()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/Task.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common; 5 | 6 | import com.google.gson.JsonElement; 7 | 8 | public interface Task { 9 | int run(ScriptConfig.BoundCommand command, JobControl jobControl); 10 | 11 | /** Sends a return value to the given script function call. Returns true if response succeeds. */ 12 | default boolean sendResponse(long functionCallId, JsonElement returnValue, boolean finalReply) { 13 | return false; 14 | } 15 | 16 | /** Sends an exception to the given script function call. Returns true if response succeeds. */ 17 | default boolean sendException(long functionCallId, ExceptionInfo exception) { 18 | return false; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/mixin/ChatComponentMixin.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common.mixin; 5 | 6 | import net.minecraft.client.GuiMessageTag; 7 | import net.minecraft.client.gui.components.ChatComponent; 8 | import net.minecraft.network.chat.Component; 9 | import net.minecraft.network.chat.MessageSignature; 10 | import net.minescript.common.Minescript; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.spongepowered.asm.mixin.Mixin; 14 | import org.spongepowered.asm.mixin.injection.At; 15 | import org.spongepowered.asm.mixin.injection.Inject; 16 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 17 | 18 | @Mixin(ChatComponent.class) 19 | public class ChatComponentMixin { 20 | private static final Logger LOGGER = LoggerFactory.getLogger("ChatComponentMixin"); 21 | 22 | @Inject( 23 | at = @At("HEAD"), 24 | method = 25 | "addMessage(Lnet/minecraft/network/chat/Component;Lnet/minecraft/network/chat/MessageSignature;Lnet/minecraft/client/GuiMessageTag;)V", 26 | cancellable = true) 27 | private void addMessage( 28 | Component message, MessageSignature signature, GuiMessageTag tag, CallbackInfo ci) { 29 | if (Minescript.onClientChatReceived(message)) { 30 | ci.cancel(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/mixin/ChatScreenMixin.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common.mixin; 5 | 6 | import net.minecraft.client.gui.components.EditBox; 7 | import net.minecraft.client.gui.screens.ChatScreen; 8 | import net.minescript.common.Minescript; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.Shadow; 11 | import org.spongepowered.asm.mixin.injection.At; 12 | import org.spongepowered.asm.mixin.injection.Inject; 13 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 14 | 15 | @Mixin(ChatScreen.class) 16 | public class ChatScreenMixin { 17 | @Shadow protected EditBox input; 18 | 19 | @Inject(at = @At("TAIL"), method = "init()V") 20 | protected void init(CallbackInfo ci) { 21 | Minescript.setChatScreenInput(this.input); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/mixin/ClientPacketListenerMixin.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common.mixin; 5 | 6 | import java.util.List; 7 | import net.minecraft.client.Minecraft; 8 | import net.minecraft.client.multiplayer.ClientPacketListener; 9 | import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; 10 | import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; 11 | import net.minecraft.network.protocol.game.ClientboundDamageEventPacket; 12 | import net.minecraft.network.protocol.game.ClientboundExplodePacket; 13 | import net.minecraft.network.protocol.game.ClientboundTakeItemEntityPacket; 14 | import net.minescript.common.Minescript; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | import org.spongepowered.asm.mixin.Mixin; 18 | import org.spongepowered.asm.mixin.injection.At; 19 | import org.spongepowered.asm.mixin.injection.Inject; 20 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 21 | 22 | @Mixin(ClientPacketListener.class) 23 | public abstract class ClientPacketListenerMixin { 24 | private static final Logger LOGGER = LoggerFactory.getLogger("ClientPacketListenerMixin"); 25 | 26 | @Inject( 27 | at = @At("TAIL"), 28 | method = "handleAddEntity(Lnet/minecraft/network/protocol/game/ClientboundAddEntityPacket;)V", 29 | cancellable = false) 30 | private void handleAddEntity(ClientboundAddEntityPacket packet, CallbackInfo ci) { 31 | var minecraft = Minecraft.getInstance(); 32 | if (!minecraft.isSameThread()) { 33 | return; 34 | } 35 | var level = minecraft.level; 36 | var entity = level.getEntity(packet.getId()); 37 | if (entity == null) { 38 | LOGGER.warn( 39 | "AddEntity event got null entity with ID {} at ({}, {}, {})", 40 | packet.getId(), 41 | packet.getX(), 42 | packet.getY(), 43 | packet.getZ()); 44 | return; 45 | } 46 | Minescript.onAddEntityEvent(entity); 47 | } 48 | 49 | @Inject( 50 | at = @At("HEAD"), 51 | method = 52 | "handleBlockUpdate(Lnet/minecraft/network/protocol/game/ClientboundBlockUpdatePacket;)V", 53 | cancellable = false) 54 | private void handleBlockUpdate(ClientboundBlockUpdatePacket packet, CallbackInfo ci) { 55 | var minecraft = Minecraft.getInstance(); 56 | if (!minecraft.isSameThread()) { 57 | return; 58 | } 59 | Minescript.onBlockUpdateEvent(packet.getPos(), packet.getBlockState()); 60 | } 61 | 62 | @Inject( 63 | at = @At("HEAD"), 64 | method = 65 | "handleTakeItemEntity(Lnet/minecraft/network/protocol/game/ClientboundTakeItemEntityPacket;)V", 66 | cancellable = false) 67 | private void handleTakeItemEntity(ClientboundTakeItemEntityPacket packet, CallbackInfo ci) { 68 | var minecraft = Minecraft.getInstance(); 69 | if (!minecraft.isSameThread()) { 70 | return; 71 | } 72 | var level = minecraft.level; 73 | var player = level.getEntity(packet.getPlayerId()); 74 | if (player == null) { 75 | LOGGER.warn("TakeItemEntity event got null player with ID {}", packet.getPlayerId()); 76 | return; 77 | } 78 | var item = level.getEntity(packet.getItemId()); 79 | if (item == null) { 80 | LOGGER.warn("TakeItemEntity event got null item with ID {}", packet.getItemId()); 81 | return; 82 | } 83 | Minescript.onTakeItemEvent(player, item, packet.getAmount()); 84 | } 85 | 86 | @Inject( 87 | at = @At("HEAD"), 88 | method = 89 | "handleDamageEvent(Lnet/minecraft/network/protocol/game/ClientboundDamageEventPacket;)V", 90 | cancellable = false) 91 | private void handleDamageEvent(ClientboundDamageEventPacket packet, CallbackInfo ci) { 92 | var minecraft = Minecraft.getInstance(); 93 | if (!minecraft.isSameThread()) { 94 | return; 95 | } 96 | var level = minecraft.level; 97 | var entity = level.getEntity(packet.entityId()); 98 | if (entity == null) { 99 | LOGGER.warn("Damage event got null item with ID {}", packet.entityId()); 100 | return; 101 | } 102 | var cause = level.getEntity(packet.sourceCauseId()); 103 | var source = packet.getSource(level); 104 | var sourceType = source == null ? null : source.type(); 105 | var sourceMessage = sourceType == null ? null : sourceType.msgId(); 106 | Minescript.onDamageEvent(entity, cause, sourceMessage); 107 | } 108 | 109 | @Inject( 110 | at = @At("HEAD"), 111 | method = "handleExplosion(Lnet/minecraft/network/protocol/game/ClientboundExplodePacket;)V", 112 | cancellable = false) 113 | private void handleExplosion(ClientboundExplodePacket packet, CallbackInfo ci) { 114 | var minecraft = Minecraft.getInstance(); 115 | if (!minecraft.isSameThread()) { 116 | return; 117 | } 118 | // 1.21.2+ dropped support for packet.getToBlow() in ClientboundExplodePacket. 119 | Minescript.onExplosionEvent(packet.center().x, packet.center().y, packet.center().z, List.of()); 120 | } 121 | 122 | // TODO(maxuser): How to trigger this event? 123 | /* 124 | @Inject( 125 | at = @At("HEAD"), 126 | method = 127 | "handleBlockDestruction(Lnet/minecraft/network/protocol/game/ClientboundBlockDestructionPacket;)V", 128 | cancellable = false) 129 | private void handleBlockDestruction(ClientboundBlockDestructionPacket packet, CallbackInfo ci) { 130 | var minecraft = Minecraft.getInstance(); 131 | if (!minecraft.isSameThread()) { 132 | return; 133 | } 134 | var level = minecraft.level; 135 | LOGGER.info( 136 | "BlockDestruction of {} at {} progress={}", 137 | level.getBlockState(packet.getPos()), 138 | packet.getId(), 139 | packet.getPos(), 140 | packet.getProgress()); 141 | } 142 | */ 143 | 144 | // TODO(maxuser): Not yet tested. 145 | /* 146 | @Inject( 147 | at = @At("HEAD"), 148 | method = 149 | "handlePlayerCombatKill(Lnet/minecraft/network/protocol/game/ClientboundPlayerCombatKillPacket;)V", 150 | cancellable = false) 151 | private void handlePlayerCombatKill(ClientboundPlayerCombatKillPacket packet, CallbackInfo ci) { 152 | var minecraft = Minecraft.getInstance(); 153 | if (!minecraft.isSameThread()) { 154 | return; 155 | } 156 | var level = minecraft.level; 157 | LOGGER.info( 158 | "PlayerCombatKill: {} `{}`", 159 | level.getEntity(packet.getPlayerId()).getName().getString(), 160 | packet.getMessage()); 161 | } 162 | */ 163 | 164 | // Only applies to signed books, not writeable books. 165 | /* 166 | @Inject( 167 | at = @At("HEAD"), 168 | method = "handleOpenBook(Lnet/minecraft/network/protocol/game/ClientboundOpenBookPacket;)V", 169 | cancellable = false) 170 | private void handleOpenBook(ClientboundOpenBookPacket packet, CallbackInfo ci) { 171 | var minecraft = Minecraft.getInstance(); 172 | if (!minecraft.isSameThread()) { 173 | return; 174 | } 175 | LOGGER.info("OpenBook {}", packet.getHand()); 176 | } 177 | */ 178 | } 179 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/mixin/KeyMappingMixin.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common.mixin; 5 | 6 | import com.mojang.blaze3d.platform.InputConstants; 7 | import net.minecraft.client.KeyMapping; 8 | import net.minescript.common.Minescript; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.Shadow; 11 | import org.spongepowered.asm.mixin.injection.At; 12 | import org.spongepowered.asm.mixin.injection.Inject; 13 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 14 | 15 | @Mixin(KeyMapping.class) 16 | public abstract class KeyMappingMixin { 17 | @Shadow 18 | public abstract String getName(); 19 | 20 | // TODO(maxuser): Clicking the button in the Key Binds screen to reset all bindings does not 21 | // trigger setKey(...) below, but it seems it should since KeyBindsScreen.init() calls 22 | // `keyMapping.setKey(keyMapping.getDefaultKey())` for all key mappings. The workaround is to 23 | // restart Minecraft after clicking the "reset all" button so that Minescript has up-to-date key 24 | // bindings. 25 | 26 | @Inject( 27 | at = @At("TAIL"), 28 | method = 29 | "(Ljava/lang/String;Lcom/mojang/blaze3d/platform/InputConstants$Type;ILjava/lang/String;)V") 30 | public void init( 31 | String name, InputConstants.Type type, int keyCode, String category, CallbackInfo ci) { 32 | Minescript.setKeyBind(name, type.getOrCreate(keyCode)); 33 | } 34 | 35 | @Inject(at = @At("HEAD"), method = "setKey(Lcom/mojang/blaze3d/platform/InputConstants$Key;)V") 36 | public void setKey(InputConstants.Key key, CallbackInfo ci) { 37 | Minescript.setKeyBind(this.getName(), key); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/mixin/KeyboardHandlerMixin.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common.mixin; 5 | 6 | import static net.minescript.common.Minescript.ENTER_KEY; 7 | import static net.minescript.common.Minescript.config; 8 | 9 | import net.minecraft.client.KeyboardHandler; 10 | import net.minecraft.client.Minecraft; 11 | import net.minescript.common.Minescript; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.spongepowered.asm.mixin.Mixin; 15 | import org.spongepowered.asm.mixin.injection.At; 16 | import org.spongepowered.asm.mixin.injection.Inject; 17 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 18 | 19 | @Mixin(KeyboardHandler.class) 20 | public class KeyboardHandlerMixin { 21 | private static final Logger LOGGER = LoggerFactory.getLogger("KeyboardHandlerMixin"); 22 | 23 | private static int KEY_ACTION_DOWN = 1; 24 | private static int KEY_ACTION_REPEAT = 2; 25 | private static int KEY_ACTION_UP = 0; 26 | 27 | @Inject(at = @At("HEAD"), method = "keyPress(JIIII)V", cancellable = true) 28 | private void keyPress( 29 | long window, int key, int scanCode, int action, int modifiers, CallbackInfo ci) { 30 | Minescript.onKeyboardEvent(key, scanCode, action, modifiers); 31 | var screen = Minecraft.getInstance().screen; 32 | if (screen == null) { 33 | Minescript.onKeyInput(key); 34 | } else if ((key == ENTER_KEY || key == config.secondaryEnterKeyCode()) 35 | && action == KEY_ACTION_DOWN 36 | && Minescript.onKeyboardKeyPressed(screen, key)) { 37 | ci.cancel(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /common/src/main/java/net/minescript/common/mixin/MouseHandlerMixin.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.common.mixin; 5 | 6 | import net.minecraft.client.MouseHandler; 7 | import net.minescript.common.Minescript; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.spongepowered.asm.mixin.Mixin; 11 | import org.spongepowered.asm.mixin.Shadow; 12 | import org.spongepowered.asm.mixin.injection.At; 13 | import org.spongepowered.asm.mixin.injection.Inject; 14 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 15 | 16 | @Mixin(MouseHandler.class) 17 | public abstract class MouseHandlerMixin { 18 | private static final Logger LOGGER = LoggerFactory.getLogger("MouseHandlerMixin"); 19 | 20 | @Shadow 21 | public abstract double xpos(); 22 | 23 | @Shadow 24 | public abstract double ypos(); 25 | 26 | @Inject(at = @At("HEAD"), method = "onPress(JIII)V", cancellable = false) 27 | private void onPress(long window, int button, int action, int modifiers, CallbackInfo ci) { 28 | Minescript.onMouseClick(button, action, modifiers, this.xpos(), this.ypos()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /common/src/main/resources/copy_blocks.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | # SPDX-License-Identifier: GPL-3.0-only 3 | 4 | # WARNING: This file is generated from the Minescript jar file. This file will 5 | # be overwritten automatically when Minescript updates to a new version. If you 6 | # make edits to this file, make sure to save a backup copy when upgrading to a 7 | # new version of Minescript. 8 | 9 | r"""copy_blocks v4.0 distributed via Minescript jar file 10 | 11 | Requires: 12 | minescript v3.0 13 | 14 | Usage: \copy X1 Y1 Z1 X2 Y2 Z2 [LABEL] [no_limit] 15 | 16 | Copies blocks within the rectangular box from (X1, Y1, Z1) to 17 | (X2, Y2, Z2), similar to the coordinates passed to the /fill 18 | command. LABEL is optional, allowing a set of blocks to be 19 | named. 20 | 21 | By default, attempts to copy a region covering more than 22 | 1600 chunks are disallowed. This limit can be relaxed by 23 | passing `no_limit`. 24 | """ 25 | 26 | import minescript 27 | import os 28 | import sys 29 | 30 | from minescript import echo, BlockPack 31 | from minescript_runtime import JavaException 32 | 33 | def main(args): 34 | if len(args) not in (6, 7, 8): 35 | echo( 36 | "Error: copy command requires 6 params of type integer " 37 | "(plus optional params for label and `no_limit`)") 38 | return 39 | 40 | safety_limit = True 41 | if "no_limit" in args: 42 | args.remove("no_limit") 43 | safety_limit = False 44 | 45 | x1 = int(args[0]) 46 | y1 = int(args[1]) 47 | z1 = int(args[2]) 48 | 49 | x2 = int(args[3]) 50 | y2 = int(args[4]) 51 | z2 = int(args[5]) 52 | 53 | if len(args) == 6: 54 | label = "__default__" 55 | else: 56 | label = args[6].replace("/", "_").replace("\\", "_").replace(" ", "_") 57 | 58 | blockpacks_dir = os.path.join("minescript", "blockpacks") 59 | if not os.path.exists(blockpacks_dir): 60 | os.makedirs(blockpacks_dir) 61 | 62 | copy_file = os.path.join(blockpacks_dir, label + ".zip") 63 | 64 | try: 65 | blockpack = BlockPack.read_world( 66 | (x1, y1, z1), (x2, y2, z2), offset=(-x1, -y1, -z1), 67 | comments={"name": label, "source command": f"copy {' '.join(sys.argv[1:])}"}, 68 | safety_limit=safety_limit) 69 | except JavaException as e: 70 | echo(e.message) 71 | return 72 | blockpack.write_file(copy_file, relative_to_cwd=True) 73 | file_size_str = "{:,}".format(os.stat(copy_file).st_size) 74 | echo( 75 | f"Copied volume {abs(x1 - x2) + 1} * {abs(y1 - y2) + 1} * {abs(z1 - z2) + 1} to " 76 | f"{copy_file} ({file_size_str} bytes).") 77 | del blockpack 78 | 79 | 80 | if __name__ == "__main__": 81 | main(sys.argv[1:]) 82 | -------------------------------------------------------------------------------- /common/src/main/resources/eval.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | # SPDX-License-Identifier: GPL-3.0-only 3 | 4 | # WARNING: This file is generated from the Minescript jar file. This file will 5 | # be overwritten automatically when Minescript updates to a new version. If you 6 | # make edits to this file, make sure to save a backup copy when upgrading to a 7 | # new version of Minescript. 8 | 9 | r"""eval v4.0 distributed via Minescript jar file 10 | 11 | Usage: 12 | \eval [ [ ...]] 13 | 14 | Executes (and optional subsequent lines 15 | , , etc) as either a Python expression (code that 16 | can appear on the right-hand side of an assignment, in which 17 | case the value is echoed to the chat screen) or Python 18 | statements (e.g. a `for` loop). 19 | 20 | Functions from minescript.py are available automatically without 21 | qualification. 22 | 23 | Examples: 24 | Print information about nearby entities to the chat screen: 25 | \eval "entities()" 26 | 27 | Print the names of nearby entities to the chat screen: 28 | \eval "for e in entities(): echo(e['name'])" 29 | 30 | Import `time` module, sleep 3 seconds, and take a screenshot: 31 | \eval "import time" "time.sleep(3)" "screenshot()" 32 | 33 | """ 34 | 35 | # `from ... import *` is normally considered poor form because of namespace 36 | # pollution. But it's desirable in this case because it allows single-line 37 | # Python code that's entered in the Minecraft chat screen to omit the module 38 | # prefix for brevity. And brevity is important for this use case. 39 | from minescript import * 40 | from typing import Any 41 | import builtins 42 | import sys 43 | 44 | def run(python_code: str) -> None: 45 | """Executes python_code as an expression or statements. 46 | 47 | Args: 48 | python_code: Python expression or statements (newline-delimited) 49 | """ 50 | # Try to evaluate as an expression. 51 | try: 52 | print(builtins.eval(python_code), file=sys.stderr) 53 | return 54 | except SyntaxError: 55 | pass 56 | 57 | # Fall back to executing as statements. 58 | builtins.exec(python_code) 59 | 60 | 61 | if __name__ == "__main__": 62 | if len(sys.argv) < 2: 63 | print( 64 | f"eval.py: Expected at least 1 parameter, instead got {len(sys.argv) - 1}: {sys.argv[1:]}", 65 | file=sys.stderr) 66 | print(r"Usage: \eval [ [ ...]]", file=sys.stderr) 67 | sys.exit(1) 68 | 69 | run("\n".join(sys.argv[1:])) 70 | -------------------------------------------------------------------------------- /common/src/main/resources/help.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | # SPDX-License-Identifier: GPL-3.0-only 3 | 4 | # WARNING: This file is generated from the Minescript jar file. This file will 5 | # be overwritten automatically when Minescript updates to a new version. If you 6 | # make edits to this file, make sure to save a backup copy when upgrading to a 7 | # new version of Minescript. 8 | 9 | r"""help v4.0 distributed via Minescript jar file 10 | 11 | Usage: \help SCRIPT_NAME 12 | 13 | Prints documentation string for the given script. 14 | """ 15 | 16 | import os 17 | import sys 18 | 19 | def ResolveScriptName(name): 20 | python_dirs = os.environ["MINESCRIPT_COMMAND_PATH"].split(os.pathsep) 21 | for dirname in python_dirs: 22 | script_filename = os.path.join(dirname, name) 23 | if os.path.exists(script_filename): 24 | return script_filename 25 | return None 26 | 27 | 28 | def ReadDocString(script_name): 29 | nlines = 0 30 | src = "" 31 | script_path = ResolveScriptName(script_name) 32 | short_name = script_name.split(".py")[0] 33 | if script_path is None: 34 | print(f'Command "{short_name}" not found.', file=sys.stderr) 35 | return None 36 | try: 37 | script = open(script_path) 38 | except FileNotFoundError as e: 39 | print(f'Script named "{script_name}" not found.', file=sys.stderr) 40 | return None 41 | docstr_start_quote = None 42 | while nlines < 100: 43 | nlines += 1 44 | line = script.readline() 45 | if not line: 46 | break 47 | if docstr_start_quote is None: 48 | if not line.strip() or line.startswith("#"): 49 | continue 50 | if line[:3] in ('"""', "'''"): 51 | docstr_start_quote = line[:3] 52 | elif line[:4] in ('r"""', "r'''"): 53 | docstr_start_quote = line[1:4] 54 | else: 55 | break 56 | src += line 57 | if line.rstrip().endswith(docstr_start_quote): 58 | return eval(src) 59 | print(f'No documentation found for "{short_name}".', file=sys.stderr) 60 | print(f'(source location: "{script_path}")', file=sys.stderr) 61 | return None 62 | 63 | def run(argv): 64 | if len(argv) != 2: 65 | print(__doc__, file=sys.stderr) 66 | return 0 if len(argv) == 1 else 1 67 | 68 | script_name = argv[1] if argv[1].endswith(".py") else argv[1] + ".py" 69 | docstr = ReadDocString(script_name) 70 | if docstr is None: 71 | return 0 72 | print(docstr, file=sys.stderr) 73 | return 0 74 | 75 | if __name__ == "__main__": 76 | sys.exit(run(sys.argv)) 77 | -------------------------------------------------------------------------------- /common/src/main/resources/minescript.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "net.minescript.common.mixin", 5 | "refmap": "${mod_id}.refmap.json", 6 | "compatibilityLevel": "JAVA_18", 7 | "mixins": [ 8 | "ChatComponentMixin", 9 | "ChatScreenMixin", 10 | "ClientPacketListenerMixin", 11 | "KeyboardHandlerMixin", 12 | "KeyMappingMixin", 13 | "MouseHandlerMixin" 14 | ], 15 | "client": [ 16 | ], 17 | "injectors": { 18 | "defaultRequire": 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /common/src/main/resources/pack.mcmeta: -------------------------------------------------------------------------------- 1 | { 2 | "pack": { 3 | "description": "${mod_name}", 4 | "pack_format": 8 5 | } 6 | } -------------------------------------------------------------------------------- /common/src/main/resources/paste.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | # SPDX-License-Identifier: GPL-3.0-only 3 | 4 | # WARNING: This file is generated from the Minescript jar file. This file will 5 | # be overwritten automatically when Minescript updates to a new version. If you 6 | # make edits to this file, make sure to save a backup copy when upgrading to a 7 | # new version of Minescript. 8 | 9 | r"""paste v4.0 distributed via Minescript jar file 10 | 11 | Requires: 12 | minescript v3.0 13 | 14 | Usage: \paste X Y Z [LABEL] 15 | 16 | Pastes blocks at location (X, Y, Z) that were previously copied 17 | via \copy. When the optional param LABEL is given, blocks are 18 | pasted from the most recent copy command with the same 19 | LABEL given, otherwise blocks are pasted from the most 20 | recent copy command with no label given. 21 | """ 22 | 23 | import minescript 24 | import os 25 | import re 26 | import sys 27 | 28 | from minescript import echo, getblocklist, BlockPack 29 | 30 | def is_eligible_for_paste(x, z, dx, dz, safety_limit) -> bool: 31 | sample_blocks_by_chunk = [] 32 | for xchunk in range(x, x + dx, 16): 33 | for zchunk in range(z, z + dz, 16): 34 | sample_blocks_by_chunk.append((xchunk, 0, zchunk)) 35 | num_chunks = len(sample_blocks_by_chunk) 36 | if safety_limit and num_chunks > 1600: 37 | echo( 38 | f"`paste` command exceeded soft limit of 1600 chunks " 39 | f"(region covers {num_chunks} chunks; override this safety check with `no_limit`).") 40 | return False 41 | echo(f"Checking {num_chunks} chunk(s) for load status...") 42 | blocks = getblocklist(sample_blocks_by_chunk) 43 | num_unloaded_blocks = 0 44 | for block in blocks: 45 | if not block or block == "minecraft:void_air": 46 | num_unloaded_blocks += 1 47 | if num_unloaded_blocks > 0: 48 | echo( 49 | f"{num_unloaded_blocks} of {num_chunks} chunks are not loaded " 50 | "within the requested `paste` volume. Cancelling paste."); 51 | return False 52 | echo( 53 | f"All {num_chunks} chunk(s) loaded within the requested `paste` volume; " 54 | "pasting blocks...") 55 | 56 | return True 57 | 58 | 59 | def main(args): 60 | if len(args) not in (3, 4, 5): 61 | echo( 62 | "Error: paste command requires 3 params of type integer " 63 | "(plus optional params for label and `no_limit`)") 64 | return 65 | 66 | safety_limit = True 67 | if "no_limit" in args: 68 | args.remove("no_limit") 69 | safety_limit = False 70 | 71 | x = int(args[0]) 72 | y = int(args[1]) 73 | z = int(args[2]) 74 | 75 | if len(args) == 3: 76 | label = "__default__" 77 | else: 78 | label = args[3].replace("/", "_").replace("\\", "_").replace(" ", "_") 79 | 80 | # BlockPacks (stored in .zip files) take precedence over legacy .txt files containing setblock 81 | # commands. 82 | blockpack_filename = os.path.join("minescript", "blockpacks", label + ".zip") 83 | legacy_txt_filename = os.path.join("minescript", "copies", label + ".txt") 84 | if os.path.isfile(blockpack_filename): 85 | blockpack = BlockPack.read_file(blockpack_filename, relative_to_cwd=True) 86 | min_block, max_block = blockpack.block_bounds() 87 | dx = max_block[0] - min_block[0] 88 | dz = max_block[2] - min_block[2] 89 | if not is_eligible_for_paste(x, z, dx, dz, safety_limit): 90 | return 91 | blockpack.write_world(offset=(x, y, z)) 92 | del blockpack 93 | elif os.path.isfile(legacy_txt_filename): 94 | paste_file = open(legacy_txt_filename) 95 | copy_command_re = re.compile( 96 | "# copy ([-0-9]+) ([-0-9]+) ([-0-9]+) ([-0-9]+) ([-0-9]+) ([-0-9]+)") 97 | for line in paste_file.readlines(): 98 | line = line.rstrip() 99 | if line.startswith("#"): 100 | m = copy_command_re.match(line) 101 | if m: 102 | x1 = int(m.group(1)) 103 | y1 = int(m.group(2)) 104 | z1 = int(m.group(3)) 105 | x2 = int(m.group(4)) 106 | y2 = int(m.group(5)) 107 | z2 = int(m.group(6)) 108 | dx = max(x1, x2) - min(x1, x2) 109 | dz = max(z1, z2) - min(z1, z2) 110 | if not is_eligible_for_paste(x, z, dx, dz, safety_limit): 111 | return 112 | continue 113 | 114 | fields = line.split(" ") 115 | if fields[0] == "/setblock": 116 | # Apply coordinate offsets: 117 | fields[1] = str(int(fields[1]) + x) 118 | fields[2] = str(int(fields[2]) + y) 119 | fields[3] = str(int(fields[3]) + z) 120 | minescript.execute(" ".join(fields)) 121 | elif fields[0] == "/fill": 122 | # Apply coordinate offsets: 123 | fields[1] = str(int(fields[1]) + x) 124 | fields[2] = str(int(fields[2]) + y) 125 | fields[3] = str(int(fields[3]) + z) 126 | fields[4] = str(int(fields[4]) + x) 127 | fields[5] = str(int(fields[5]) + y) 128 | fields[6] = str(int(fields[6]) + z) 129 | minescript.execute(" ".join(fields)) 130 | else: 131 | echo( 132 | "Error: paste works only with setblock and fill commands, " 133 | "but got the following instead:\n") 134 | echo(line) 135 | return 136 | else: 137 | echo(f"Error: blockpack file for `{label}` not found at {blockpack_filename}") 138 | 139 | if __name__ == "__main__": 140 | main(sys.argv[1:]) 141 | -------------------------------------------------------------------------------- /common/src/main/resources/posix_config.txt: -------------------------------------------------------------------------------- 1 | # Lines starting with "#" are ignored. 2 | 3 | python="/usr/bin/python3" 4 | -------------------------------------------------------------------------------- /common/src/main/resources/version.txt: -------------------------------------------------------------------------------- 1 | ${version} 2 | -------------------------------------------------------------------------------- /common/src/main/resources/windows_config.txt: -------------------------------------------------------------------------------- 1 | # Lines starting with "#" are ignored. 2 | 3 | python="%userprofile%\AppData\Local\Microsoft\WindowsApps\python3.exe" 4 | -------------------------------------------------------------------------------- /docs/v1.19/README.md: -------------------------------------------------------------------------------- 1 | ## Minescript v1.19 docs 2 | 3 | Table of contents: 4 | 5 | - [In-game commands](#in-game-commands) 6 | - [Command basics](#command-basics) 7 | - [General commands](#general-commands) 8 | - [Advanced commands](#advanced-commands) 9 | - [Python API](#python-api) 10 | - [Script input](#script-input) 11 | - [Script output](#script-output) 12 | - [minescript module](#minescript-module) 13 | 14 | ## In-game commands 15 | 16 | ### Command basics 17 | 18 | Minescript commands are available from the in-game chat console. They’re 19 | similar to Minecraft commands that start with a slash (`/`), but Minescript 20 | commands start with a backslash (`\`) instead. 21 | 22 | Python scripts in the **minescript** folder (within the **minecraft** folder) 23 | can be run as commands from the in-game chat by placing a backslash (`\`) 24 | before the script name and dropping the `.py` from the end of the filename. 25 | E.g. a Python script `minecraft/minescript/build_fortress.py` can be run as a 26 | Minescript command by entering `\build_fortress` in the in-game chat. If the 27 | in-game chat is hidden, you can display it by pressing `\`, similar to 28 | pressing `t` or `/` in vanilla Minecraft. Parameters can be passed to 29 | Minescript script commands if the Python script supports command-line 30 | parameters. (See example at [Script input](#script-input) below). 31 | 32 | Minescript commands that take a sequence of `X Y Z` parameters can take tilde 33 | syntax to specify locations relative to the local player, e.g. `~ ~ ~` or `~-1 34 | ~2 ~-3`. Alternatively, params can be specified as `$x`, `$y`, and `$z` for the 35 | local player’s x, y, or z coordinate, respectively. (`$` syntax is particularly 36 | useful to specify coordinates that don’t appear as 3 consecutive `X Y Z` 37 | coordinates.) 38 | 39 | Optional command parameters below are shown in square brackets like this: 40 | [EXAMPLE]. (But leave out the brackets when entering actual commands.) 41 | 42 | ### General commands 43 | 44 | #### ls 45 | *Usage:* `\ls` 46 | 47 | List all available Minescript commands, including both Minescript built-in 48 | commands and Python scripts in the **minescript** folder. 49 | 50 | #### help 51 | *Usage:* `\help NAME` 52 | 53 | Prints documentation for the given script or command name. 54 | 55 | Since: v1.19.2 56 | 57 | #### copy 58 | *Usage:* `\copy X1 Y1 Z1 X2 Y2 Z2 [LABEL]` 59 | 60 | Copies blocks within the rectangular box from (X1, Y1, Z1) to (X2, Y2, Z2), 61 | similar to the coordinates passed to the /fill command. LABEL is optional, 62 | allowing a set of blocks to be named. 63 | 64 | See [\paste](#paste). 65 | 66 | #### paste 67 | *Usage:* `\paste X Y Z [LABEL]` 68 | 69 | Pastes blocks at location (X, Y, Z) that were previously copied via \copy. When 70 | the optional param LABEL is given, blocks are pasted from the most recent copy 71 | command with the same LABEL given, otherwise blocks are pasted from the most 72 | recent copy command with no label given. 73 | 74 | Note that \copy and \paste can be run within different worlds to copy a region 75 | from one world into another. 76 | 77 | See [\copy](#copy). 78 | 79 | #### jobs 80 | *Usage:* `\jobs` 81 | 82 | Lists the currently running Minescript jobs. 83 | 84 | #### suspend 85 | *Usage:* `\suspend [JOB_ID]` 86 | 87 | Suspends currently running Minescript job or jobs. If JOB_ID is specified, the 88 | job with that integer ID is suspended. Otherwise, all currently running 89 | Minescript jobs are suspended. 90 | 91 | See [\resume](#resume). 92 | 93 | #### z 94 | *Usage:* `\z [JOB_ID]` 95 | 96 | Alias for `\suspend`. 97 | 98 | #### resume 99 | *Usage:* `\resume [JOB_ID]` 100 | 101 | Resumes currently suspended Minescript job or jobs. If JOB_ID is specified, the 102 | job with that integer ID is resumed if currently suspended. If JOB_ID is not 103 | specified, all currently suspended Minescript jobs are resumed. 104 | 105 | See [\suspend](#suspend). 106 | 107 | #### killjob 108 | *Usage:* `\killjob JOB_ID` 109 | 110 | Kills the currently running or suspended Minescript job corresponding to 111 | JOB_ID. The special value -1 can be specified to kill all currently running or 112 | suspended Minescript jobs. 113 | 114 | #### undo 115 | *Usage:* `\undo` 116 | 117 | Undoes all the `/setblock` and `/fill` commands run by the last Minescript 118 | command by restoring the blocks present beforehand. This is useful if a 119 | Minescript command accidentally destroyed a build and you’d like to revert to 120 | the state of your build before the last command. `\undo` can be run multiple 121 | times to undo the build changes from multiple recent Minescript commands. 122 | 123 | ***Note:*** *Some block state may be lost when undoing a Minescript command, such as 124 | commands specified within command blocks.* 125 | 126 | ### Advanced commands 127 | 128 | #### minescript_commands_per_cycle 129 | *Usage:* `\minescript_commands_per_cycle NUMBER` 130 | 131 | Specifies the number of Minescript-generated Minecraft commands to run per 132 | Minescript processing cycle. The higher the number, the faster the script will 133 | run. 134 | 135 | ***Note:*** *Setting this value too high will make Minecraft less responsive and 136 | possibly crash.* 137 | 138 | Default is 15. 139 | 140 | #### minescript_ticks_per_cycle 141 | *Usage:* `\minescript_ticks_per_cycle NUMBER` 142 | 143 | Specifies the number of Minecraft game ticks to wait per Minecraft processing 144 | cycle. The lower the number, down to a minimum of 1, the faster the script will 145 | run. 146 | 147 | ***Note:*** *Setting this value too low will make Minecraft less responsive and 148 | possibly crash.* 149 | 150 | Default is 3. 151 | 152 | ## Python API 153 | 154 | ### Script input 155 | 156 | Parameters can be passed from Minecraft as input to a Python script. For 157 | example, consider this Python script located at 158 | `minecraft/minescript/build_fortress.py`: 159 | 160 | ``` 161 | import sys 162 | 163 | def BuildFortress(width, height, length): 164 | ... 165 | 166 | width = sys.argv[1] 167 | height = sys.argv[2] 168 | length = sys.argv[3] 169 | # Or more succinctly: 170 | # width, height, length = sys.argv[1:] 171 | BuildFortress(width, height, length) 172 | ``` 173 | 174 | The above script can be run from the Minecraft in-game chat as: 175 | 176 | ``` 177 | \build_fortress 100 50 200 178 | ``` 179 | 180 | That command passes parameters that set `width` to `100`, `height` to `50`, and 181 | `length` to `200`. 182 | 183 | ### Script output 184 | 185 | When a Minescript Python script prints to standard output (`sys.stdout`), the 186 | output text is sent to the Minecraft chat as if entered by the user: 187 | 188 | ``` 189 | # Sends a chat message that's visible to 190 | # all players in the world: 191 | print("hi, friends!") 192 | 193 | # Runs a command to set the block under the 194 | # current player to yellow concrete (assuming 195 | # you have permission to run commands): 196 | print("/setblock ~ ~-1 ~ yellow_concrete") 197 | ``` 198 | 199 | When a script prints to standard error (`sys.stderr`), the output text is 200 | printed to the Minecraft chat, but is visible only to you: 201 | 202 | ``` 203 | # Prints a message to the in-game chat that's 204 | # visible only to you: 205 | print("Note to self...", file=sys.stderr) 206 | ``` 207 | 208 | ### minescript module 209 | 210 | From a Python script in the **minescript** folder, import the `minescript` 211 | module: 212 | 213 | ``` 214 | import minescript 215 | ``` 216 | 217 | #### player_position 218 | *Usage:* `player_position(done_callback=None)` 219 | 220 | Gets the local player’s position. 221 | 222 | *Args:* 223 | 224 | - `done_callback`: if given, return immediately and call 225 | done_callback(return_value) asynchronously when return_value is ready 226 | 227 | *Returns:* 228 | 229 | - if `done_callback` is None, returns player’s position as [x, y, z] 230 | 231 | *Example:* 232 | ``` 233 | x, y, z = minescript.player_position() 234 | ``` 235 | 236 | #### getblock 237 | *Usage:* `getblock(x: int, y: int, z: int, done_callback=None)` 238 | 239 | Gets the type of block at position (x, y, z). 240 | 241 | *Args:* 242 | 243 | - `done_callback`: if given, return immediately and call 244 | `done_callback(return_value)` asynchronously when return_value is ready 245 | 246 | *Returns:* 247 | 248 | - if done_callback is None, returns the block type at (x, y, z) as a string 249 | 250 | *Example:* 251 | 252 | ``` 253 | block_type = minescript.getblock(x, y, z) 254 | ``` 255 | 256 | #### await_loaded_region 257 | *Usage:* `await_loaded_region(x1: int, z1: int, x2: int, z2: int, done_callback=None)` 258 | 259 | Notifies the caller when all the chunks in the region from (x1, z1) to (x2, z2) 260 | are fully loaded. This function is useful for making sure that a region is 261 | fully loaded before setting or filling blocks within it. 262 | 263 | *Args:* 264 | 265 | - `done_callback`: if given, return immediately and call 266 | `done_callback(return_value)` asynchronously when return_value is ready 267 | 268 | *Returns:* 269 | 270 | - if done_callback is None, returns True when the requested region is fully 271 | loaded. 272 | 273 | *Examples:* 274 | 275 | [1] Don't do any work until the region is done loading (synchronous / blocking 276 | call): 277 | 278 | ``` 279 | print("About to wait for region to load...", file=sys.stderr) 280 | 281 | # Load all chunks within (x, z) bounds (0, 0) and (320, 160): 282 | minescript.await_loaded_region(0, 0, 320, 160) 283 | 284 | print("Region finished loading.", file=sys.stderr) 285 | ``` 286 | 287 | [2] Continue doing work on the main thread while the region loads in the 288 | background (asynchronous / non-blocking call): 289 | 290 | ``` 291 | import minescript 292 | import threading 293 | 294 | lock = threading.Lock() 295 | 296 | def on_region_loaded(loaded): 297 | if loaded: 298 | print("Region loaded ok.", file=sys.stderr) 299 | else: 300 | print("Region failed to load.", file=sys.stderr) 301 | lock.release() 302 | 303 | # Acquire the lock, to be released later by on_region_loaded(). 304 | lock.acquire() 305 | 306 | # Calls on_region_loaded(...) when region finishes 307 | # loading all chunks within (x, z) bounds (0, 0) 308 | # and (320, 160): 309 | minescript.await_loaded_region( 310 | 0, 0, 320, 160, on_region_loaded) 311 | 312 | print("Do other work while region loads...", file=sys.stderr) 313 | 314 | print("Now wait for region to finish loading...", file=stderr) 315 | lock.acquire() 316 | 317 | print("Do more work now that region finished loading...", file=stderr) 318 | ``` 319 | 320 | -------------------------------------------------------------------------------- /docs/v2.0/README.md: -------------------------------------------------------------------------------- 1 | ## Minescript v2.0 docs 2 | 3 | Table of contents: 4 | 5 | - [In-game commands](#in-game-commands) 6 | - [Command basics](#command-basics) 7 | - [General commands](#general-commands) 8 | - [Advanced commands](#advanced-commands) 9 | - [Python API](#python-api) 10 | - [Script input](#script-input) 11 | - [Script output](#script-output) 12 | - [minescript module](#minescript-module) 13 | 14 | ## In-game commands 15 | 16 | ### Command basics 17 | 18 | Minescript commands are available from the in-game chat console. They’re 19 | similar to Minecraft commands that start with a slash (`/`), but Minescript 20 | commands start with a backslash (`\`) instead. 21 | 22 | Python scripts in the **minescript** folder (within the **minecraft** folder) 23 | can be run as commands from the in-game chat by placing a backslash (`\`) 24 | before the script name and dropping the `.py` from the end of the filename. 25 | E.g. a Python script `minecraft/minescript/build_fortress.py` can be run as a 26 | Minescript command by entering `\build_fortress` in the in-game chat. If the 27 | in-game chat is hidden, you can display it by pressing `\`, similar to 28 | pressing `t` or `/` in vanilla Minecraft. Parameters can be passed to 29 | Minescript script commands if the Python script supports command-line 30 | parameters. (See example at [Script input](#script-input) below). 31 | 32 | Minescript commands that take a sequence of `X Y Z` parameters can take tilde 33 | syntax to specify locations relative to the local player, e.g. `~ ~ ~` or `~-1 34 | ~2 ~-3`. Alternatively, params can be specified as `$x`, `$y`, and `$z` for the 35 | local player’s x, y, or z coordinate, respectively. (`$` syntax is particularly 36 | useful to specify coordinates that don’t appear as 3 consecutive `X Y Z` 37 | coordinates.) 38 | 39 | Optional command parameters below are shown in square brackets like this: 40 | [EXAMPLE]. (But leave out the brackets when entering actual commands.) 41 | 42 | ### General commands 43 | 44 | #### ls 45 | *Usage:* `\ls` 46 | 47 | List all available Minescript commands, including both Minescript built-in 48 | commands and Python scripts in the **minescript** folder. 49 | 50 | #### help 51 | *Usage:* `\help NAME` 52 | 53 | Prints documentation for the given script or command name. 54 | 55 | Since: v1.19.2 56 | 57 | #### copy 58 | *Usage:* `\copy X1 Y1 Z1 X2 Y2 Z2 [LABEL]` 59 | 60 | Copies blocks within the rectangular box from (X1, Y1, Z1) to (X2, Y2, Z2), 61 | similar to the coordinates passed to the /fill command. LABEL is optional, 62 | allowing a set of blocks to be named. 63 | 64 | See [\paste](#paste). 65 | 66 | #### paste 67 | *Usage:* `\paste X Y Z [LABEL]` 68 | 69 | Pastes blocks at location (X, Y, Z) that were previously copied via \copy. When 70 | the optional param LABEL is given, blocks are pasted from the most recent copy 71 | command with the same LABEL given, otherwise blocks are pasted from the most 72 | recent copy command with no label given. 73 | 74 | Note that \copy and \paste can be run within different worlds to copy a region 75 | from one world into another. 76 | 77 | See [\copy](#copy). 78 | 79 | #### jobs 80 | *Usage:* `\jobs` 81 | 82 | Lists the currently running Minescript jobs. 83 | 84 | #### suspend 85 | *Usage:* `\suspend [JOB_ID]` 86 | 87 | Suspends currently running Minescript job or jobs. If JOB_ID is specified, the 88 | job with that integer ID is suspended. Otherwise, all currently running 89 | Minescript jobs are suspended. 90 | 91 | See [\resume](#resume). 92 | 93 | #### z 94 | *Usage:* `\z [JOB_ID]` 95 | 96 | Alias for `\suspend`. 97 | 98 | #### resume 99 | *Usage:* `\resume [JOB_ID]` 100 | 101 | Resumes currently suspended Minescript job or jobs. If JOB_ID is specified, the 102 | job with that integer ID is resumed if currently suspended. If JOB_ID is not 103 | specified, all currently suspended Minescript jobs are resumed. 104 | 105 | See [\suspend](#suspend). 106 | 107 | #### killjob 108 | *Usage:* `\killjob JOB_ID` 109 | 110 | Kills the currently running or suspended Minescript job corresponding to 111 | JOB_ID. The special value -1 can be specified to kill all currently running or 112 | suspended Minescript jobs. 113 | 114 | #### undo 115 | *Usage:* `\undo` 116 | 117 | Undoes all the `/setblock` and `/fill` commands run by the last Minescript 118 | command by restoring the blocks present beforehand. This is useful if a 119 | Minescript command accidentally destroyed a build and you’d like to revert to 120 | the state of your build before the last command. `\undo` can be run multiple 121 | times to undo the build changes from multiple recent Minescript commands. 122 | 123 | ***Note:*** *Some block state may be lost when undoing a Minescript command, such as 124 | commands specified within command blocks and items in chests.* 125 | 126 | ### Advanced commands 127 | 128 | #### minescript_commands_per_cycle 129 | *Usage:* `\minescript_commands_per_cycle NUMBER` 130 | 131 | Specifies the number of Minescript-generated Minecraft commands to run per 132 | Minescript processing cycle. The higher the number, the faster the script will 133 | run. 134 | 135 | ***Note:*** *Setting this value too high will make Minecraft less responsive and 136 | possibly crash.* 137 | 138 | Default is 15. 139 | 140 | #### minescript_ticks_per_cycle 141 | *Usage:* `\minescript_ticks_per_cycle NUMBER` 142 | 143 | Specifies the number of Minecraft game ticks to wait per Minecraft processing 144 | cycle. The lower the number, down to a minimum of 1, the faster the script will 145 | run. 146 | 147 | ***Note:*** *Setting this value too low will make Minecraft less responsive and 148 | possibly crash.* 149 | 150 | Default is 3. 151 | 152 | #### minescript_incremental_command_suggestions 153 | *Usage:* `\minescript_incremental_command_suggestions BOOL` 154 | 155 | Enables or disables printing of incremental command suggestions to the in-game 156 | chat as the user types a Minescript command. 157 | 158 | Default is false. 159 | 160 | Since: v2.0 (in prior versions, incremental command suggestions were 161 | unconditionally enabled) 162 | 163 | ## Python API 164 | 165 | ### Script input 166 | 167 | Parameters can be passed from Minecraft as input to a Python script. For 168 | example, consider this Python script located at 169 | `minecraft/minescript/build_fortress.py`: 170 | 171 | ``` 172 | import sys 173 | 174 | def BuildFortress(width, height, length): 175 | ... 176 | 177 | width = sys.argv[1] 178 | height = sys.argv[2] 179 | length = sys.argv[3] 180 | # Or more succinctly: 181 | # width, height, length = sys.argv[1:] 182 | BuildFortress(width, height, length) 183 | ``` 184 | 185 | The above script can be run from the Minecraft in-game chat as: 186 | 187 | ``` 188 | \build_fortress 100 50 200 189 | ``` 190 | 191 | That command passes parameters that set `width` to `100`, `height` to `50`, and 192 | `length` to `200`. 193 | 194 | ### Script output 195 | 196 | Minescript Python scripts can write outputs using `sys.stdout` and 197 | `sys.stderr`, or they can use functions defined in `minescript.py` (see 198 | [`echo`](#echo), [`chat`](#chat), and [`exec`](#exec)). The 199 | `minescript.py` functions are recommended going forward, but output via 200 | `sys.stdout` and `sys.stderr` are provided for backward compatibility with 201 | earlier versions of Minescript. 202 | 203 | Printing to standard output (`sys.stdout`) outputs text to the Minecraft chat 204 | as if entered by the user: 205 | 206 | ``` 207 | # Sends a chat message that's visible to 208 | # all players in the world: 209 | print("hi, friends!") 210 | 211 | # Since Minescript v2.0 this can be written as: 212 | import minescript 213 | minescript.chat("hi, friends!") 214 | 215 | # Runs a command to set the block under the 216 | # current player to yellow concrete (assuming 217 | # you have permission to run commands): 218 | print("/setblock ~ ~-1 ~ yellow_concrete") 219 | 220 | # Since Minescript v2.0 this can be written as: 221 | minescript.exec("/setblock ~ ~-1 ~ yellow_concrete") 222 | ``` 223 | 224 | ***Note:*** *`exec` has been renamed to `execute` in Minescript v2.1 to avoid 225 | confusion with Python's built-in `exec`.* 226 | 227 | When a script prints to standard error (`sys.stderr`), the output text is 228 | printed to the Minecraft chat, but is visible only to you: 229 | 230 | ``` 231 | # Prints a message to the in-game chat that's 232 | # visible only to you: 233 | print("Note to self...", file=sys.stderr) 234 | 235 | # Since Minescript v2.0 this can be written as: 236 | minescript.echo("Note to self...") 237 | ``` 238 | 239 | ### minescript module 240 | 241 | From a Python script in the **minescript** folder, import the `minescript` 242 | module: 243 | 244 | ``` 245 | import minescript 246 | ``` 247 | 248 | #### exec 249 | *Usage:* `exec(command: str)` 250 | 251 | Executes the given Minecraft or Minescript command. 252 | 253 | If command doesn't already start with a slash or backslash, automatically 254 | prepends a slash. Ignores leading and trailing whitespace, and ignores empty 255 | commands. 256 | 257 | *Note: This is named `execute` in Minescript 2.1. The old name is still 258 | available in v2.1, but is deprecated and will be removed in a future version.* 259 | 260 | Since: v2.0 261 | 262 | 263 | #### echo 264 | *Usage:* `echo(message: Any)` 265 | 266 | Echoes message to the chat. 267 | 268 | The echoed message is visible only to the local player. 269 | 270 | Since: v2.0 271 | 272 | 273 | #### chat 274 | *Usage:* `chat(message: str)` 275 | 276 | Sends the given message to the chat. 277 | 278 | If message starts with a slash or backslash, automatically prepends a space 279 | so that the message is sent as a chat and not executed as a command. Ignores 280 | empty messages. 281 | 282 | Since: v2.0 283 | 284 | 285 | #### player_position 286 | *Usage:* `player_position(done_callback=None)` 287 | 288 | Gets the local player’s position. 289 | 290 | *Args:* 291 | 292 | - `done_callback`: if given, return immediately and call 293 | done_callback(return_value) asynchronously when return_value is ready 294 | 295 | *Returns:* 296 | 297 | - if `done_callback` is None, returns player’s position as [x, y, z] 298 | 299 | *Example:* 300 | ``` 301 | x, y, z = minescript.player_position() 302 | ``` 303 | 304 | #### player_hand_items 305 | *Usage:* `player_hand_items(done_callback=None)` 306 | 307 | Gets the items in the local player's hands. 308 | 309 | *Args:* 310 | 311 | - `done_callback`: if given, return immediately and call 312 | `done_callback(return_value)` asynchronously when return_value is ready. 313 | 314 | *Returns:* 315 | 316 | - If `done_callback` is `None`, returns items in player's inventory as list of 317 | items where each item is a dict: `{"item": str, "count": int, "nbt": str}` 318 | 319 | Since: v2.0 320 | 321 | 322 | #### player_inventory 323 | *Usage:* `player_inventory(done_callback=None)` 324 | 325 | Gets the items in the local player's inventory. 326 | 327 | *Args:* 328 | 329 | - `done_callback`: if given, return immediately and call 330 | `done_callback(return_value)` asynchronously when return_value is ready 331 | 332 | *Returns:* 333 | 334 | - If `done_callback` is `None`, returns items in player's inventory as list of 335 | items where each item is a dict: `{"item": str, "count": int, "nbt": str}` 336 | 337 | Since: v2.0 338 | 339 | 340 | #### getblock 341 | *Usage:* `getblock(x: int, y: int, z: int, done_callback=None)` 342 | 343 | Gets the type of block at position (x, y, z). 344 | 345 | *Args:* 346 | 347 | - `done_callback`: if given, return immediately and call 348 | `done_callback(return_value)` asynchronously when return_value is ready 349 | 350 | *Returns:* 351 | 352 | - if done_callback is None, returns the block type at (x, y, z) as a string 353 | 354 | *Example:* 355 | 356 | ``` 357 | block_type = minescript.getblock(x, y, z) 358 | ``` 359 | 360 | #### await_loaded_region 361 | *Usage:* `await_loaded_region(x1: int, z1: int, x2: int, z2: int, done_callback=None)` 362 | 363 | Notifies the caller when all the chunks in the region from (x1, z1) to (x2, z2) 364 | are fully loaded. This function is useful for making sure that a region is 365 | fully loaded before setting or filling blocks within it. 366 | 367 | *Args:* 368 | 369 | - `done_callback`: if given, return immediately and call 370 | `done_callback(return_value)` asynchronously when return_value is ready 371 | 372 | *Returns:* 373 | 374 | - if done_callback is None, returns True when the requested region is fully 375 | loaded. 376 | 377 | *Examples:* 378 | 379 | [1] Don't do any work until the region is done loading (synchronous / blocking 380 | call): 381 | 382 | ``` 383 | print("About to wait for region to load...", file=sys.stderr) 384 | 385 | # Load all chunks within (x, z) bounds (0, 0) and (320, 160): 386 | minescript.await_loaded_region(0, 0, 320, 160) 387 | 388 | print("Region finished loading.", file=sys.stderr) 389 | ``` 390 | 391 | [2] Continue doing work on the main thread while the region loads in the 392 | background (asynchronous / non-blocking call): 393 | 394 | ``` 395 | import minescript 396 | import threading 397 | 398 | lock = threading.Lock() 399 | 400 | def on_region_loaded(loaded): 401 | if loaded: 402 | print("Region loaded ok.", file=sys.stderr) 403 | else: 404 | print("Region failed to load.", file=sys.stderr) 405 | lock.release() 406 | 407 | # Acquire the lock, to be released later by on_region_loaded(). 408 | lock.acquire() 409 | 410 | # Calls on_region_loaded(...) when region finishes 411 | # loading all chunks within (x, z) bounds (0, 0) 412 | # and (320, 160): 413 | minescript.await_loaded_region( 414 | 0, 0, 320, 160, on_region_loaded) 415 | 416 | print("Do other work while region loads...", file=sys.stderr) 417 | 418 | print("Now wait for region to finish loading...", file=stderr) 419 | lock.acquire() 420 | 421 | print("Do more work now that region finished loading...", file=stderr) 422 | ``` 423 | 424 | 425 | #### register_chat_message_listener 426 | *Usage:* `register_chat_message_listener(listener: Callable[[str], None])` 427 | 428 | Registers a listener for receiving chat messages. One listener allowed per job. 429 | 430 | Listener receives both incoming and outgoing chat messages. 431 | 432 | *Args:* 433 | 434 | - `listener`: callable that repeatedly accepts a string representing chat messages 435 | 436 | Since: v2.0 437 | 438 | 439 | #### unregister_chat_message_listener 440 | *Usage:* `unregister_chat_message_listener()` 441 | 442 | Unegisters a chat message listener, if any, for the currently running job. 443 | 444 | *Returns:* 445 | 446 | - `True` if successfully unregistered a listener, `False` otherwise. 447 | 448 | Since: v2.0 449 | 450 | -------------------------------------------------------------------------------- /fabric/.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 | # macos 28 | 29 | *.DS_Store 30 | 31 | # fabric 32 | 33 | run/ 34 | -------------------------------------------------------------------------------- /fabric/build.gradle: -------------------------------------------------------------------------------- 1 | // Based on MultiLoader-Template: 2 | // https://github.com/jaredlll08/MultiLoader-Template/blob/1.21/fabric/build.gradle 3 | plugins { 4 | id 'multiloader-loader' 5 | id 'fabric-loom' 6 | } 7 | dependencies { 8 | minecraft "com.mojang:minecraft:${minecraft_version}" 9 | mappings loom.layered { 10 | officialMojangMappings() 11 | parchment("org.parchmentmc.data:parchment-${parchment_minecraft}:${parchment_version}@zip") 12 | } 13 | modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}" 14 | modImplementation "net.fabricmc.fabric-api:fabric-api:${fabric_version}" 15 | } 16 | 17 | loom { 18 | def aw = project(':common').file("src/main/resources/${mod_id}.accesswidener") 19 | if (aw.exists()) { 20 | accessWidenerPath.set(aw) 21 | } 22 | mixin { 23 | defaultRefmapName.set("${mod_id}.refmap.json") 24 | } 25 | runs { 26 | client { 27 | client() 28 | setConfigName('Fabric Client') 29 | ideConfigGenerated(true) 30 | runDir('runs/client') 31 | } 32 | server { 33 | server() 34 | setConfigName('Fabric Server') 35 | ideConfigGenerated(true) 36 | runDir('runs/server') 37 | } 38 | } 39 | } 40 | 41 | javadoc { 42 | exclude '**/*.swp' 43 | } 44 | -------------------------------------------------------------------------------- /fabric/src/main/java/net/minescript/fabric/FabricPlatform.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.fabric; 5 | 6 | import net.minescript.common.Platform; 7 | 8 | class FabricPlatform implements Platform { 9 | @Override 10 | public String modLoaderName() { 11 | return "Fabric"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /fabric/src/main/java/net/minescript/fabric/MinescriptFabricClientMod.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.fabric; 5 | 6 | import net.fabricmc.api.ClientModInitializer; 7 | import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientChunkEvents; 8 | import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; 9 | import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; 10 | import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; 11 | import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; 12 | import net.fabricmc.fabric.api.client.screen.v1.ScreenKeyboardEvents; 13 | import net.minecraft.client.Minecraft; 14 | import net.minecraft.client.gui.screens.ChatScreen; 15 | import net.minecraft.client.gui.screens.Screen; 16 | import net.minescript.common.Minescript; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | public final class MinescriptFabricClientMod implements ClientModInitializer { 21 | private static final Logger LOGGER = LoggerFactory.getLogger("MinescriptFabricClientMod"); 22 | 23 | @Override 24 | public void onInitializeClient() { 25 | LOGGER.info("(minescript) Minescript mod starting..."); 26 | 27 | ClientChunkEvents.CHUNK_LOAD.register((world, chunk) -> Minescript.onChunkLoad(world, chunk)); 28 | ClientChunkEvents.CHUNK_UNLOAD.register( 29 | (world, chunk) -> Minescript.onChunkUnload(world, chunk)); 30 | 31 | Minescript.init(new FabricPlatform()); 32 | ClientTickEvents.START_WORLD_TICK.register(world -> Minescript.onClientWorldTick()); 33 | ScreenEvents.AFTER_INIT.register(this::afterInitScreen); 34 | WorldRenderEvents.END.register(this::onRender); 35 | } 36 | 37 | private void afterInitScreen(Minecraft client, Screen screen, int windowWidth, int windowHeight) { 38 | if (screen instanceof ChatScreen) { 39 | ScreenKeyboardEvents.allowKeyPress(screen) 40 | .register( 41 | (_screen, key, scancode, modifiers) -> 42 | !Minescript.onKeyboardKeyPressed(_screen, key)); 43 | } 44 | } 45 | 46 | private void onRender(WorldRenderContext context) { 47 | Minescript.onRenderWorld(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /fabric/src/main/java/net/minescript/fabric/MinescriptFabricMod.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.fabric; 5 | 6 | import net.fabricmc.api.ModInitializer; 7 | 8 | public final class MinescriptFabricMod implements ModInitializer { 9 | @Override 10 | public void onInitialize() {} 11 | } 12 | -------------------------------------------------------------------------------- /fabric/src/main/resources/assets/modid/minescript-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxuser0/minescript/ebbb7a5f6e714ada3bb3b3d299f6c2aa5ebebedd/fabric/src/main/resources/assets/modid/minescript-logo.png -------------------------------------------------------------------------------- /fabric/src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "${mod_id}", 4 | "version": "${version}", 5 | 6 | "name": "${mod_name}", 7 | "description": "${description}", 8 | "authors": [ 9 | "${mod_author}" 10 | ], 11 | "contact": { 12 | "homepage": "https://minescript.net/", 13 | "sources": "https://github.com/maxuser0/minescript", 14 | "issues": "https://github.com/maxuser0/minescript/issues" 15 | }, 16 | 17 | "license": "${license}", 18 | "icon": "assets/modid/minescript-logo.png", 19 | 20 | "environment": "client", 21 | "entrypoints": { 22 | "main": [ 23 | "net.minescript.fabric.MinescriptFabricMod" 24 | ], 25 | "client": [ 26 | "net.minescript.fabric.MinescriptFabricClientMod" 27 | ] 28 | }, 29 | 30 | "mixins": [ 31 | "minescript.mixins.json", 32 | "minescript.fabric.mixins.json" 33 | ], 34 | 35 | "depends": { 36 | "fabricloader": ">=${fabric_loader_version}", 37 | "fabric": "*", 38 | "fabric-screen-api-v1": "*", 39 | "minecraft": "${minecraft_version}", 40 | "java": ">=${java_version}" 41 | }, 42 | "suggests": { 43 | "another-mod": "*" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /fabric/src/main/resources/minescript.fabric.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "net.minescript.fabric.mixin", 5 | "refmap": "${mod_id}.refmap.json", 6 | "compatibilityLevel": "JAVA_21", 7 | "mixins": [], 8 | "client": [ 9 | ], 10 | "injectors": { 11 | "defaultRequire": 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /forge/.gitattributes: -------------------------------------------------------------------------------- 1 | # Disable autocrlf on generated files, they always generate with LF 2 | # Add any extra files or paths here to make git stop saying they 3 | # are changed when only line endings change. 4 | src/generated/**/.cache/cache text eol=lf 5 | src/generated/**/*.json text eol=lf 6 | -------------------------------------------------------------------------------- /forge/.gitignore: -------------------------------------------------------------------------------- 1 | # eclipse 2 | bin 3 | *.launch 4 | .settings 5 | .metadata 6 | .classpath 7 | .project 8 | 9 | # idea 10 | out 11 | *.ipr 12 | *.iws 13 | *.iml 14 | .idea 15 | 16 | # gradle 17 | build 18 | .gradle 19 | 20 | # other 21 | eclipse 22 | run 23 | 24 | # Files from Forge MDK 25 | forge*changelog.txt 26 | -------------------------------------------------------------------------------- /forge/build.gradle: -------------------------------------------------------------------------------- 1 | // Based on MultiLoader-Template: 2 | // https://github.com/jaredlll08/MultiLoader-Template/blob/1.21/forge/build.gradle 3 | 4 | plugins { 5 | id 'multiloader-loader' 6 | id 'net.minecraftforge.gradle' version '[6.0.24,6.2)' 7 | id 'org.spongepowered.mixin' version '0.7-SNAPSHOT' 8 | } 9 | base { 10 | // NOTE(maxuser): Use mod_id for consistency with Fabric and NeoForge. 11 | archivesName = "${mod_id}-forge-${minecraft_version}" 12 | } 13 | mixin { 14 | config("${mod_id}.mixins.json") 15 | config("${mod_id}.forge.mixins.json") 16 | } 17 | 18 | minecraft { 19 | mappings channel: 'official', version: minecraft_version 20 | 21 | copyIdeResources = true //Calls processResources when in dev 22 | 23 | reobf = false // Forge 1.20.6+ uses official mappings at runtime, so we shouldn't reobf from official to SRG 24 | 25 | // Automatically enable forge AccessTransformers if the file exists 26 | // This location is hardcoded in Forge and can not be changed. 27 | // https://github.com/MinecraftForge/MinecraftForge/blob/be1698bb1554f9c8fa2f58e32b9ab70bc4385e60/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModFile.java#L123 28 | // Forge still uses SRG names during compile time, so we cannot use the common AT's 29 | def at = file('src/main/resources/META-INF/accesstransformer.cfg') 30 | if (at.exists()) { 31 | accessTransformer = at 32 | } 33 | 34 | runs { 35 | client { 36 | workingDirectory file('runs/client') 37 | ideaModule "${rootProject.name}.${project.name}.main" 38 | taskName 'runClient' 39 | mods { 40 | modClientRun { 41 | source sourceSets.main 42 | } 43 | } 44 | } 45 | 46 | server { 47 | workingDirectory file('runs/server') 48 | ideaModule "${rootProject.name}.${project.name}.main" 49 | taskName 'runServer' 50 | mods { 51 | modServerRun { 52 | source sourceSets.main 53 | } 54 | } 55 | } 56 | 57 | data { 58 | workingDirectory file('runs/data') 59 | ideaModule "${rootProject.name}.${project.name}.main" 60 | args '--mod', mod_id, '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/') 61 | taskName 'Data' 62 | mods { 63 | modDataRun { 64 | source sourceSets.main 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | sourceSets.main.resources.srcDir 'src/generated/resources' 72 | 73 | dependencies { 74 | minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}" 75 | annotationProcessor("org.spongepowered:mixin:0.8.5-SNAPSHOT:processor") 76 | 77 | // Forge's hack fix 78 | implementation('net.sf.jopt-simple:jopt-simple:5.0.4') { version { strictly '5.0.4' } } 79 | } 80 | 81 | publishing { 82 | publications { 83 | mavenJava(MavenPublication) { 84 | fg.component(it) 85 | } 86 | } 87 | } 88 | 89 | sourceSets.each { 90 | def dir = layout.buildDirectory.dir("sourcesSets/$it.name") 91 | it.output.resourcesDir = dir 92 | it.java.destinationDirectory = dir 93 | } 94 | 95 | jar { 96 | manifest { 97 | attributes([ 98 | 'MixinConfigs': "${mod_id}.mixins.json,${mod_id}.forge.mixins.json", 99 | ]) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /forge/src/main/java/net/minescript/forge/Constants.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.forge; 5 | 6 | final class Constants { 7 | public static final String MODID = "minescript"; 8 | 9 | public static int KEY_ACTION_DOWN = 1; 10 | public static int KEY_ACTION_REPEAT = 2; 11 | public static int KEY_ACTION_UP = 0; 12 | } 13 | -------------------------------------------------------------------------------- /forge/src/main/java/net/minescript/forge/ForgePlatform.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.forge; 5 | 6 | import net.minescript.common.Platform; 7 | 8 | class ForgePlatform implements Platform { 9 | @Override 10 | public String modLoaderName() { 11 | return "Forge"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /forge/src/main/java/net/minescript/forge/MinescriptForgeClientMod.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.forge; 5 | 6 | import static net.minescript.common.Minescript.ENTER_KEY; 7 | import static net.minescript.common.Minescript.config; 8 | 9 | import net.minecraft.client.Minecraft; 10 | import net.minecraft.client.multiplayer.ClientLevel; 11 | import net.minecraftforge.api.distmarker.Dist; 12 | import net.minecraftforge.client.event.InputEvent; 13 | import net.minecraftforge.client.event.ScreenEvent; 14 | import net.minecraftforge.event.TickEvent; 15 | import net.minecraftforge.event.level.ChunkEvent; 16 | import net.minecraftforge.eventbus.api.SubscribeEvent; 17 | import net.minecraftforge.fml.LogicalSide; 18 | import net.minecraftforge.fml.common.Mod; 19 | import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; 20 | import net.minescript.common.Minescript; 21 | import org.apache.logging.log4j.LogManager; 22 | import org.apache.logging.log4j.Logger; 23 | 24 | public class MinescriptForgeClientMod { 25 | private static final Logger LOGGER = LogManager.getLogger(); 26 | 27 | public MinescriptForgeClientMod() {} 28 | 29 | @Mod.EventBusSubscriber( 30 | modid = Constants.MODID, 31 | bus = Mod.EventBusSubscriber.Bus.MOD, 32 | value = Dist.CLIENT) 33 | public static class ClientModEvents { 34 | @SubscribeEvent 35 | public static void onClientSetup(FMLClientSetupEvent event) { 36 | LOGGER.info("(minescript) Minescript mod starting..."); 37 | Minescript.init(new ForgePlatform()); 38 | } 39 | } 40 | 41 | @Mod.EventBusSubscriber(Dist.CLIENT) 42 | public static class ClientEvents { 43 | @SubscribeEvent 44 | public static void onKeyboardKeyPressedEvent(ScreenEvent.KeyPressed event) { 45 | if (Minescript.onKeyboardKeyPressed(event.getScreen(), event.getKeyCode())) { 46 | event.setCanceled(true); 47 | } 48 | } 49 | 50 | @SubscribeEvent 51 | public static void onKeyInputEvent(InputEvent.Key event) { 52 | var key = event.getKey(); 53 | var action = event.getAction(); 54 | var screen = Minecraft.getInstance().screen; 55 | if (screen == null) { 56 | Minescript.onKeyInput(key); 57 | } else if ((key == ENTER_KEY || key == config.secondaryEnterKeyCode()) 58 | && action == Constants.KEY_ACTION_DOWN 59 | && Minescript.onKeyboardKeyPressed(screen, key)) { 60 | event.setCanceled(true); 61 | } 62 | } 63 | 64 | @SubscribeEvent 65 | public static void onChunkLoadEvent(ChunkEvent.Load event) { 66 | if (event.getLevel() instanceof ClientLevel) { 67 | Minescript.onChunkLoad(event.getLevel(), event.getChunk()); 68 | } 69 | } 70 | 71 | @SubscribeEvent 72 | public static void onChunkUnloadEvent(ChunkEvent.Unload event) { 73 | if (event.getLevel() instanceof ClientLevel) { 74 | Minescript.onChunkUnload(event.getLevel(), event.getChunk()); 75 | } 76 | } 77 | 78 | @SubscribeEvent 79 | public static void onWorldTick(TickEvent.LevelTickEvent event) { 80 | if (event.side == LogicalSide.CLIENT && event.phase == TickEvent.Phase.START) { 81 | Minescript.onClientWorldTick(); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /forge/src/main/java/net/minescript/forge/MinescriptForgeMod.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.forge; 5 | 6 | import net.minecraftforge.fml.common.Mod; 7 | 8 | @Mod(Constants.MODID) 9 | public class MinescriptForgeMod {} 10 | -------------------------------------------------------------------------------- /forge/src/main/java/net/minescript/forge/mixin/LevelRendererMixin.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.forge.mixin; 5 | 6 | import com.mojang.blaze3d.resource.GraphicsResourceAllocator; 7 | import net.minecraft.client.Camera; 8 | import net.minecraft.client.DeltaTracker; 9 | import net.minecraft.client.renderer.GameRenderer; 10 | import net.minecraft.client.renderer.LevelRenderer; 11 | import net.minescript.common.Minescript; 12 | import org.joml.Matrix4f; 13 | import org.spongepowered.asm.mixin.Mixin; 14 | import org.spongepowered.asm.mixin.injection.At; 15 | import org.spongepowered.asm.mixin.injection.Inject; 16 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 17 | 18 | @Mixin(LevelRenderer.class) 19 | public class LevelRendererMixin { 20 | @Inject( 21 | at = @At("TAIL"), 22 | method = 23 | "renderLevel(Lcom/mojang/blaze3d/resource/GraphicsResourceAllocator;Lnet/minecraft/client/DeltaTracker;ZLnet/minecraft/client/Camera;Lnet/minecraft/client/renderer/GameRenderer;Lorg/joml/Matrix4f;Lorg/joml/Matrix4f;)V") 24 | public void renderLevel( 25 | GraphicsResourceAllocator graphicsResourceAllocator, 26 | DeltaTracker deltaTracker, 27 | boolean bl, 28 | Camera camera, 29 | GameRenderer gameRenderer, 30 | Matrix4f matrix4f, 31 | Matrix4f matrix4f2, 32 | CallbackInfo ci) { 33 | Minescript.onRenderWorld(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /forge/src/main/resources/META-INF/mods.toml: -------------------------------------------------------------------------------- 1 | # There are several mandatory fields (#mandatory), and many more that are optional (#optional). 2 | # The overall format is standard TOML format, v0.5.0. 3 | # Note that there are a couple of TOML lists in this file. 4 | # Find more information on toml format here: https://github.com/toml-lang/toml 5 | # The name of the mod loader type to load - for regular FML @Mod mods it should be javafml 6 | modLoader="javafml" #mandatory 7 | # A version range to match for said mod loader - for regular FML @Mod it will be the forge version 8 | loaderVersion="[41,)" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions. 9 | # The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. 10 | # Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. 11 | license="GPL-3.0-only" 12 | # A URL to refer people to when problems occur with this mod 13 | #issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional 14 | # A list of mods - how many allowed here is determined by the individual mod loader 15 | [[mods]] #mandatory 16 | # The modid of the mod 17 | modId="minescript" #mandatory 18 | version="${version}" #mandatory 19 | # A display name for the mod 20 | displayName="Minescript" #mandatory 21 | # A URL to query for updates for this mod. See the JSON update specification https://mcforge.readthedocs.io/en/latest/gettingstarted/autoupdate/ 22 | #updateJSONURL="https://change.me.example.invalid/updates.json" #optional 23 | # A URL for the "homepage" for this mod, displayed in the mod UI 24 | displayURL="https://minescript.net/" #optional 25 | # A file name (in the root of the mod JAR) containing a logo for display 26 | logoFile="minescript-logo.png" #optional 27 | # A text field displayed in the mod UI 28 | credits="Special thanks to Spiderfffun and Coolbou0427 for testing." #optional 29 | # A text field displayed in the mod UI 30 | authors="maxuser@minescript.net" #optional 31 | # The description text for the mod (multi line!) (#mandatory) 32 | description=''' 33 | Minescript is a platform for controlling and interacting with Minecraft using scripts written in the Python programming language, with support for additional languages. Visit https://minescript.net/ for documentation, examples, and downloadable scripts. 34 | ''' 35 | # A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. 36 | [[dependencies.minescript]] #optional 37 | # the modid of the dependency 38 | modId="forge" #mandatory 39 | # Does this dependency have to exist - if not, ordering below must be specified 40 | mandatory=true #mandatory 41 | # The version range of the dependency 42 | versionRange="[46,)" #mandatory 43 | # An ordering relationship for the dependency - BEFORE or AFTER required if the relationship is not mandatory 44 | ordering="NONE" 45 | # Side this dependency is applied on - BOTH, CLIENT or SERVER 46 | side="BOTH" 47 | # Here's another dependency 48 | [[dependencies.minescript]] 49 | modId="minecraft" 50 | mandatory=true 51 | # This version range declares a minimum of the current minecraft version up to but not including the next major version 52 | versionRange="[1.21,1.22)" 53 | ordering="NONE" 54 | side="BOTH" 55 | -------------------------------------------------------------------------------- /forge/src/main/resources/minescript-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxuser0/minescript/ebbb7a5f6e714ada3bb3b3d299f6c2aa5ebebedd/forge/src/main/resources/minescript-logo.png -------------------------------------------------------------------------------- /forge/src/main/resources/minescript.forge.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "net.minescript.forge.mixin", 5 | "compatibilityLevel": "JAVA_18", 6 | "mixins": [], 7 | "client": [ 8 | "LevelRendererMixin" 9 | ], 10 | "server": [], 11 | "injectors": { 12 | "defaultRequire": 1 13 | } 14 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Based on MultiLoader-Template: 2 | # https://github.com/jaredlll08/MultiLoader-Template/blob/1.21/gradle.properties 3 | 4 | # Important Notes: 5 | # Every field you add must be added to the root build.gradle expandProps map. 6 | 7 | # Project 8 | version=4.0 9 | group=net.minescript 10 | java_version=21 11 | 12 | # Common 13 | minecraft_version=1.21.5 14 | mod_name=Minescript 15 | mod_author=maxuser@minescript.net 16 | mod_id=minescript 17 | license=GPL-3.0-only 18 | credits= 19 | description=Python scripting for Minecraft 20 | minecraft_version_range=[1.21, 1.22) 21 | ## This is the version of minecraft that the 'common' project uses, you can find a list of all versions here 22 | ## https://projects.neoforged.net/neoforged/neoform 23 | neo_form_version=1.21.5-20250325.162830 24 | # The version of ParchmentMC that is used, see https://parchmentmc.org/docs/getting-started#choose-a-version for new versions 25 | parchment_minecraft=1.21 26 | parchment_version=2024.11.10 27 | 28 | # Fabric 29 | fabric_version=0.121.0+1.21.5 30 | fabric_loader_version=0.16.13 31 | 32 | # Forge 33 | forge_version=55.0.6 34 | forge_loader_version_range=[55,) 35 | 36 | # NeoForge 37 | neoforge_version=21.5.52-beta 38 | neoforge_loader_version_range=[4,) 39 | 40 | # Gradle 41 | org.gradle.jvmargs=-Xmx3G 42 | org.gradle.daemon=false 43 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxuser0/minescript/ebbb7a5f6e714ada3bb3b3d299f6c2aa5ebebedd/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.12-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 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /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 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /neoforge/build.gradle: -------------------------------------------------------------------------------- 1 | // Based on MultiLoader-Template: 2 | // https://github.com/jaredlll08/MultiLoader-Template/blob/1.21/neoforge/build.gradle 3 | plugins { 4 | id 'multiloader-loader' 5 | id 'net.neoforged.moddev' 6 | } 7 | 8 | neoForge { 9 | version = neoforge_version 10 | // Automatically enable neoforge AccessTransformers if the file exists 11 | // This location is hardcoded in FML and can not be changed. 12 | // https://github.com/neoforged/FancyModLoader/blob/a952595eaaddd571fbc53f43847680b00894e0c1/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFile.java#L118 13 | def at = project(':common').file('src/main/resources/META-INF/accesstransformer.cfg') 14 | if (at.exists()) { 15 | accessTransformers.add(at.absolutePath) 16 | } 17 | parchment { 18 | minecraftVersion = parchment_minecraft 19 | mappingsVersion = parchment_version 20 | } 21 | runs { 22 | configureEach { 23 | systemProperty('neoforge.enabledGameTestNamespaces', mod_id) 24 | ideName = "NeoForge ${it.name.capitalize()} (${project.path})" // Unify the run config names with fabric 25 | } 26 | client { 27 | client() 28 | } 29 | data { 30 | data() 31 | } 32 | server { 33 | server() 34 | } 35 | } 36 | mods { 37 | "${mod_id}" { 38 | sourceSet sourceSets.main 39 | } 40 | } 41 | } 42 | 43 | sourceSets.main.resources { srcDir 'src/generated/resources' } 44 | -------------------------------------------------------------------------------- /neoforge/src/main/java/net/minescript/neoforge/Constants.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.neoforge; 5 | 6 | final class Constants { 7 | public static final String MODID = "minescript"; 8 | 9 | public static int KEY_ACTION_DOWN = 1; 10 | public static int KEY_ACTION_REPEAT = 2; 11 | public static int KEY_ACTION_UP = 0; 12 | } 13 | -------------------------------------------------------------------------------- /neoforge/src/main/java/net/minescript/neoforge/MinescriptNeoForgeClientMod.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.neoforge; 5 | 6 | import static net.minescript.common.Minescript.ENTER_KEY; 7 | import static net.minescript.common.Minescript.config; 8 | 9 | import net.minecraft.client.Minecraft; 10 | import net.minecraft.client.multiplayer.ClientLevel; 11 | import net.minescript.common.Minescript; 12 | import net.neoforged.api.distmarker.Dist; 13 | import net.neoforged.bus.api.SubscribeEvent; 14 | import net.neoforged.fml.common.EventBusSubscriber; 15 | import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; 16 | import net.neoforged.neoforge.client.event.InputEvent; 17 | import net.neoforged.neoforge.client.event.RenderLevelStageEvent; 18 | import net.neoforged.neoforge.client.event.ScreenEvent; 19 | import net.neoforged.neoforge.event.level.ChunkEvent; 20 | import net.neoforged.neoforge.event.tick.LevelTickEvent; 21 | import org.apache.logging.log4j.LogManager; 22 | import org.apache.logging.log4j.Logger; 23 | 24 | public class MinescriptNeoForgeClientMod { 25 | private static final Logger LOGGER = LogManager.getLogger(); 26 | 27 | public MinescriptNeoForgeClientMod() {} 28 | 29 | @EventBusSubscriber( 30 | modid = Constants.MODID, 31 | bus = EventBusSubscriber.Bus.MOD, 32 | value = Dist.CLIENT) 33 | public static class ClientModEvents { 34 | @SubscribeEvent 35 | public static void onClientSetup(FMLClientSetupEvent event) { 36 | LOGGER.info("(minescript) Minescript mod starting..."); 37 | Minescript.init(new NeoForgePlatform()); 38 | } 39 | } 40 | 41 | @EventBusSubscriber(Dist.CLIENT) 42 | public static class ClientEvents { 43 | @SubscribeEvent 44 | public static void onRender(RenderLevelStageEvent event) { 45 | if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_LEVEL) { 46 | return; 47 | } 48 | Minescript.onRenderWorld(); 49 | } 50 | 51 | @SubscribeEvent 52 | public static void onKeyboardKeyPressedEvent(ScreenEvent.KeyPressed.Pre event) { 53 | if (Minescript.onKeyboardKeyPressed(event.getScreen(), event.getKeyCode())) { 54 | event.setCanceled(true); 55 | } 56 | } 57 | 58 | @SubscribeEvent 59 | public static void onKeyInputEvent(InputEvent.Key event) { 60 | var key = event.getKey(); 61 | var action = event.getAction(); 62 | var screen = Minecraft.getInstance().screen; 63 | if (screen == null) { 64 | Minescript.onKeyInput(key); 65 | } else if ((key == ENTER_KEY || key == config.secondaryEnterKeyCode()) 66 | && action == Constants.KEY_ACTION_DOWN 67 | && Minescript.onKeyboardKeyPressed(screen, key)) { 68 | // TODO(maxuser): InputEvent.Key isn't cancellable with NeoForge. 69 | // event.setCanceled(true); 70 | } 71 | } 72 | 73 | @SubscribeEvent 74 | public static void onChunkLoadEvent(ChunkEvent.Load event) { 75 | if (event.getLevel() instanceof ClientLevel) { 76 | Minescript.onChunkLoad(event.getLevel(), event.getChunk()); 77 | } 78 | } 79 | 80 | @SubscribeEvent 81 | public static void onChunkUnloadEvent(ChunkEvent.Unload event) { 82 | if (event.getLevel() instanceof ClientLevel) { 83 | Minescript.onChunkUnload(event.getLevel(), event.getChunk()); 84 | } 85 | } 86 | 87 | @SubscribeEvent 88 | public static void onWorldTick(LevelTickEvent.Pre event) { 89 | if (event.getLevel().isClientSide()) { 90 | Minescript.onClientWorldTick(); 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /neoforge/src/main/java/net/minescript/neoforge/MinescriptNeoForgeMod.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.neoforge; 5 | 6 | import net.neoforged.fml.common.Mod; 7 | 8 | @Mod(Constants.MODID) 9 | public class MinescriptNeoForgeMod {} 10 | -------------------------------------------------------------------------------- /neoforge/src/main/java/net/minescript/neoforge/NeoForgePlatform.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | package net.minescript.neoforge; 5 | 6 | import net.minescript.common.Platform; 7 | 8 | class NeoForgePlatform implements Platform { 9 | @Override 10 | public String modLoaderName() { 11 | return "NeoForge"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /neoforge/src/main/resources/META-INF/neoforge.mods.toml: -------------------------------------------------------------------------------- 1 | # There are several mandatory fields (#mandatory), and many more that are optional (#optional). 2 | # The overall format is standard TOML format, v0.5.0. 3 | # Note that there are a couple of TOML lists in this file. 4 | # Find more information on toml format here: https://github.com/toml-lang/toml 5 | # The name of the mod loader type to load - for regular FML @Mod mods it should be javafml 6 | modLoader="javafml" #mandatory 7 | # A version range to match for said mod loader - for regular FML @Mod it will be the forge version 8 | loaderVersion="${neoforge_loader_version_range}" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions. 9 | # The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. 10 | # Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. 11 | license="${license}" 12 | # A URL to refer people to when problems occur with this mod 13 | #issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional 14 | # A list of mods - how many allowed here is determined by the individual mod loader 15 | [[mods]] #mandatory 16 | # The modid of the mod 17 | modId="${mod_id}" #mandatory 18 | version="${version}" #mandatory 19 | # A display name for the mod 20 | displayName="Minescript" #mandatory 21 | # A URL to query for updates for this mod. See the JSON update specification https://mcforge.readthedocs.io/en/latest/gettingstarted/autoupdate/ 22 | #updateJSONURL="https://change.me.example.invalid/updates.json" #optional 23 | # A URL for the "homepage" for this mod, displayed in the mod UI 24 | displayURL="https://minescript.net/" #optional 25 | # A file name (in the root of the mod JAR) containing a logo for display 26 | logoFile="minescript-logo.png" #optional 27 | # A text field displayed in the mod UI 28 | credits="Special thanks to Spiderfffun and Coolbou0427 for testing." #optional 29 | # A text field displayed in the mod UI 30 | authors="maxuser@minescript.net" #optional 31 | # The description text for the mod (multi line!) (#mandatory) 32 | description=''' 33 | Minescript is a platform for controlling and interacting with Minecraft using scripts written in the Python programming language, with support for additional languages. Visit https://minescript.net/ for documentation, examples, and downloadable scripts. 34 | ''' 35 | [[mixins]] 36 | config="${mod_id}.mixins.json" 37 | # A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. 38 | [[dependencies.minescript]] #optional 39 | # the modid of the dependency 40 | modId="neoforge" #mandatory 41 | # Does this dependency have to exist - if not, ordering below must be specified 42 | type="required" 43 | # The version range of the dependency 44 | versionRange="${neoforge_loader_version_range}" #mandatory 45 | # An ordering relationship for the dependency - BEFORE or AFTER required if the relationship is not mandatory 46 | ordering="NONE" 47 | # Side this dependency is applied on - BOTH, CLIENT or SERVER 48 | side="BOTH" 49 | # Here's another dependency 50 | [[dependencies.minescript]] 51 | modId="minecraft" 52 | type="required" 53 | # This version range declares a minimum of the current minecraft version up to but not including the next major version 54 | versionRange="${minecraft_version_range}" 55 | ordering="NONE" 56 | side="BOTH" 57 | -------------------------------------------------------------------------------- /neoforge/src/main/resources/minescript-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxuser0/minescript/ebbb7a5f6e714ada3bb3b3d299f6c2aa5ebebedd/neoforge/src/main/resources/minescript-logo.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | // Based on MultiLoader-Template: 2 | // https://github.com/jaredlll08/MultiLoader-Template/blob/1.21/settings.gradle 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | mavenCentral() 7 | exclusiveContent { 8 | forRepository { 9 | maven { 10 | name = 'Fabric' 11 | url = uri('https://maven.fabricmc.net') 12 | } 13 | } 14 | filter { 15 | includeGroup('net.fabricmc') 16 | includeGroup('fabric-loom') 17 | } 18 | } 19 | exclusiveContent { 20 | forRepository { 21 | maven { 22 | name = 'Sponge' 23 | url = uri('https://repo.spongepowered.org/repository/maven-public') 24 | } 25 | } 26 | filter { 27 | includeGroupAndSubgroups("org.spongepowered") 28 | } 29 | } 30 | exclusiveContent { 31 | forRepository { 32 | maven { 33 | name = 'Forge' 34 | url = uri('https://maven.minecraftforge.net') 35 | } 36 | } 37 | filter { 38 | includeGroupAndSubgroups('net.minecraftforge') 39 | } 40 | } 41 | } 42 | } 43 | 44 | plugins { 45 | id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' 46 | } 47 | 48 | // This should match the folder name of the project, or else IDEA may complain (see https://youtrack.jetbrains.com/issue/IDEA-317606) 49 | rootProject.name = 'minescript' 50 | include('common') 51 | 52 | if (System.getenv("NO_MINESCRIPT_FABRIC_BUILD") != "1") { 53 | include('fabric') 54 | } 55 | 56 | if (System.getenv("NO_MINESCRIPT_NEOFORGE_BUILD") != "1") { 57 | include('neoforge') 58 | } 59 | 60 | if (System.getenv("NO_MINESCRIPT_FORGE_BUILD") != "1") { 61 | include('forge') 62 | } 63 | -------------------------------------------------------------------------------- /tools/find_version_number.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 4 | # SPDX-License-Identifier: MIT 5 | 6 | if [ "$1" = "-l" ]; then 7 | grep_flags="-l" 8 | shift 9 | fi 10 | version=${1:?"Error: version number required."} 11 | if [ "$2" = "-l" ]; then 12 | grep_flags="-l" 13 | shift 14 | fi 15 | 16 | find common -type f | 17 | grep -v /.gradle/ | 18 | grep -v /.git/ | 19 | grep -v /build/ | 20 | grep -v /run/ | 21 | grep -v /caches/ | 22 | grep -v /archive/ | 23 | grep -v '/\..*\.swp$' | 24 | xargs grep -F $grep_flags "$version" |grep -v "[.0-9]${version}" |grep -v "${version}[.0-9]" | 25 | sed 's/^\.\///' 26 | -------------------------------------------------------------------------------- /tools/markdown_to_html.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 4 | # SPDX-License-Identifier: MIT 5 | 6 | "Tool for converting Markdown to HTML." 7 | 8 | import markdown 9 | import re 10 | import sys 11 | 12 | html_heading_re = re.compile(r"([^<]*)") 13 | 14 | md_input = sys.stdin.read() 15 | html_output = markdown.markdown(md_input, extensions=["fenced_code"]) 16 | prev_line = None 17 | for line in html_output.splitlines(): 18 | # The Python implementation of markdown sometimes doesn't output HTML anchors 19 | # for headings. (Why?) Check the previous line of output for an anchor, and 20 | # if it's missing, generate one. 21 | m = html_heading_re.match(line) 22 | if m: 23 | title = m.group(1) 24 | anchor = re.sub("[^a-zA-Z0-9-_]", "", title.lower().replace(" ", "-")) 25 | anchor_html = f'

' 26 | if anchor_html != prev_line: 27 | print(anchor_html) 28 | 29 | # Add a line linking to the latest docs on GitHub above the table of contents. 30 | if "

Table of contents:

" in line: 31 | print( 32 | '

View docs for all versions of Minescript on ' 33 | 'GitHub' 34 | '.

') 35 | 36 | line = line.replace("\\", "\") 37 | print(line) 38 | prev_line = line 39 | -------------------------------------------------------------------------------- /tools/pydoc_to_markdown.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 4 | # SPDX-License-Identifier: MIT 5 | 6 | "Tool for converting pydoc to Markdown." 7 | 8 | import re 9 | import sys 10 | from dataclasses import dataclass 11 | from enum import Enum 12 | from typing import Any, List, Set, Dict, Tuple, Optional, Callable 13 | 14 | FUNCTION_RE = re.compile(r"^def ([a-zA-Z_0-9]+)(.*)") 15 | CLASS_RE = re.compile(r"^class ([a-zA-Z_0-9]+)") 16 | METHOD_RE = re.compile(r"^ def ([a-zA-Z_0-9]+)(.*)") 17 | METHOD_DECORATION_RE = re.compile(r"^ (@(static|class)method)$") 18 | GLOBAL_ASSIGNMENT_RE = re.compile(r"^([a-zA-Z_0-9]+)(: ([a-zA-Z_0-9.]+))? = ") 19 | CLASS_ASSIGNMENT_RE = re.compile(r"^ ([a-zA-Z_0-9]+)(: ([a-zA-Z_0-9.]+))? = ") 20 | BEGIN_TRIPLE_QUOTE = re.compile(r'^ *r?"""([^ ].*)') 21 | END_TRIPLE_QUOTE = re.compile(r'(.*)"""$') 22 | REQUIREMENT_LINE = re.compile(r'^ [a-zA-Z0-9_-]+ v.*') 23 | 24 | 25 | class GlobalEntityType(Enum): 26 | CLASS = 1 27 | FUNCTION = 2 28 | ASSIGNMENT = 4 29 | 30 | def for_display(self): 31 | return self.name.lower() 32 | 33 | 34 | class ClassMemberType(Enum): 35 | METHOD = 1 36 | ASSIGNMENT = 2 37 | 38 | def for_display(self): 39 | return self.name.lower() 40 | 41 | 42 | class CodeEntity: 43 | pass 44 | 45 | 46 | @dataclass 47 | class GlobalEntity(CodeEntity): 48 | name: str 49 | kind: GlobalEntityType 50 | func_decl: str = None 51 | decoration: str = None 52 | 53 | def fullname(self): 54 | return self.name 55 | 56 | 57 | @dataclass 58 | class ClassMember(CodeEntity): 59 | classname: str 60 | name: str 61 | kind: ClassMemberType 62 | func_decl: str = None 63 | decoration: str = None 64 | 65 | def fullname(self): 66 | return f"{self.classname}.{self.name}" 67 | 68 | 69 | def escape_for_markdown(s: str): 70 | s = re.sub(r"__", r"\_\_", s) 71 | s = re.sub(r"\*", r"\*", s) 72 | return s 73 | 74 | 75 | def rewrite_special_methods(func_decl: str) -> str: 76 | func_decl = func_decl.replace(".__init__(", "(") 77 | func_decl = re.sub( 78 | r"^([a-zA-Z0-9_]+)\.__del__\(self\)", 79 | lambda m: f"del {m.group(1).lower()}", 80 | func_decl) 81 | return func_decl 82 | 83 | def linkify_func_decl(func_decl: str, anchors: Dict[str, str]) -> str: 84 | """Return rewritten decl with arg types linked to their definitions, if available in `anchors`.""" 85 | func_decl= re.sub(r"'([a-zA-Z0-9_]+)'", r"\1", func_decl) 86 | 87 | # List of rewrites from (start, end) indices in pydoc to a replacement string. 88 | anchor_rewrites: List[Tuple[int, int, str]] = [] 89 | for m in re.finditer(r"[:>] ([a-zA-Z0-9_]+)\b", func_decl): 90 | argtype = m.group(1) 91 | anchor = anchors.get(argtype) 92 | if anchor: 93 | start = m.start() + 2 94 | end = start + len(argtype) 95 | anchor_rewrites.append((start, end, f"[{argtype}](#{anchor})")) 96 | 97 | # Sort rewrites from highest to lowest index so the later rewrites do not 98 | # invalidate earlier indices. 99 | anchor_rewrites.sort(reverse=True) 100 | for start, end, replacement in anchor_rewrites: 101 | func_decl = func_decl[:start] + replacement + func_decl[end:] 102 | 103 | return func_decl 104 | 105 | 106 | LEADING_SINGLE_UNDERSCORE_RE = re.compile("^_[^_]") 107 | 108 | def process_pydoc(code_entity: CodeEntity, pydoc: str, anchors: Dict[str, str]): 109 | if "(__internal__)" in pydoc: 110 | return 111 | 112 | if code_entity and LEADING_SINGLE_UNDERSCORE_RE.match(code_entity.name): 113 | return 114 | 115 | is_class_member = type(code_entity) is ClassMember 116 | if is_class_member and LEADING_SINGLE_UNDERSCORE_RE.match(code_entity.classname): 117 | return 118 | 119 | # List of rewrites from (start, end) indices in pydoc to a replacement string. 120 | anchor_rewrites: List[Tuple[int, int, str]] = [] 121 | for m in re.finditer(r"`[a-zA-Z0-9_.]+[^`]*`", pydoc): 122 | start = m.start() 123 | end_backtick = pydoc.find("`", start + 1) 124 | if end_backtick != -1: 125 | open_paren = pydoc.find("(", start + 1, end_backtick) 126 | if open_paren == -1: 127 | end = end_backtick 128 | else: 129 | end = open_paren 130 | symbol = pydoc[start+1:end] 131 | anchor = anchors.get(symbol) 132 | if not anchor: 133 | if is_class_member: 134 | symbol = f"{code_entity.classname}.{symbol}" 135 | elif type(code_entity) is GlobalEntity and code_entity.kind is GlobalEntityType.CLASS: 136 | symbol = f"{code_entity.name}.{symbol}" 137 | anchor = anchors.get(symbol) 138 | if anchor: 139 | anchor_rewrites.append((start, end_backtick, f"[{pydoc[start:end_backtick+1]}](#{anchor})")) 140 | 141 | # Sort rewrites from highest to lowest index so the later rewrites do not 142 | # invalidate earlier indices. 143 | anchor_rewrites.sort(reverse=True) 144 | for start, end, replacement in anchor_rewrites: 145 | pydoc = pydoc[:start] + replacement + pydoc[end + 1:] 146 | 147 | if not code_entity: 148 | # This is the module itself. Get the name from pydoc. 149 | module_name, version = pydoc.split()[0:2] 150 | if module_name == "minescript": 151 | print(f"### {module_name} module") 152 | else: 153 | print(f"### {module_name} {version}") 154 | pydoc = re.sub(r"\nUsage: ([^\n]*)", r"\n*Usage:* `\1`", pydoc) 155 | pydoc = pydoc.replace("\nUsage:", "\n*Usage:*\n") 156 | pydoc_lines = pydoc.splitlines()[1:] 157 | is_requires_block = False 158 | for line in pydoc_lines: 159 | if line.lstrip().startswith("```"): 160 | line = line .lstrip() 161 | 162 | if line.strip().startswith("Example") and line.strip().endswith(":"): 163 | print(f"\n*{line.strip()}*\n") 164 | elif line == "Requires:": 165 | print("*Requires:*\n") 166 | is_requires_block = True 167 | elif is_requires_block: 168 | if REQUIREMENT_LINE.match(line): 169 | print(f"- `{line.strip()}`") 170 | else: 171 | is_requires_block = False 172 | print(line) 173 | else: 174 | print(line) 175 | print() 176 | return 177 | 178 | prefix = "" 179 | name = code_entity.name 180 | if is_class_member: 181 | name = f"{code_entity.classname}.{name}" 182 | prefix = f"{code_entity.classname}." 183 | name = escape_for_markdown(name) 184 | 185 | heading = "####" 186 | if is_class_member: 187 | pydoc = pydoc.replace("\n ", "\n") 188 | 189 | pydoc = (pydoc 190 | .replace("\n ", "\n") 191 | .replace("\nArgs:", "\n*Args:*\n") 192 | .replace("\nReturns:\n ", "\n*Returns:*\n\n- ") 193 | .replace("\nRaises:", "\n*Raises:*\n") 194 | .replace("\nExample:", "\n*Example:*\n") 195 | .replace("\nExamples:", "\n*Examples:*\n") 196 | ) 197 | 198 | # Un-indent example text following the "Example" heading so that triple-backtick 199 | # code blocks convert properly to HTML. 200 | m = re.search(r"^\*Example(s)?:\*", pydoc, re.M) 201 | if m: 202 | example_text_start = m.start() + len(m.group(0)) 203 | pydoc = pydoc[:example_text_start] + pydoc[example_text_start:].replace("\n ", "\n") 204 | 205 | # Replace args with their backtick-quoted names. 206 | pydoc = re.sub("\n ([a-z0-9_, ]+): ", r"\n- `\1`: ", pydoc) 207 | 208 | if code_entity: 209 | decoration = f"{code_entity.decoration} " if code_entity.decoration else "" 210 | if code_entity.func_decl: 211 | func_decl = escape_for_markdown( 212 | linkify_func_decl(rewrite_special_methods(prefix + code_entity.func_decl), anchors) 213 | .replace("(cls, ", "(") 214 | .replace("(cls)", "()") 215 | .replace("(self, ", "(") 216 | .replace("(self)", "()") 217 | .rstrip(":") 218 | ) 219 | print(f"{heading} {name}\n*Usage:* {decoration}{func_decl}\n\n{pydoc}") 220 | else: 221 | print(f"{heading} {name}\n{pydoc}") 222 | else: 223 | print(f"module:\n{pydoc}") 224 | 225 | print() 226 | 227 | 228 | def parse_code_entities() -> List[Tuple[CodeEntity, str]]: 229 | global_entity = None 230 | class_member = None 231 | method_decoration = None 232 | pydoc = None 233 | 234 | # List of pairs of code entity and its pydoc string. 235 | entities: List[Tuple[CodeEntity, str]] = [] 236 | 237 | for line in sys.stdin.readlines(): 238 | if pydoc is not None: 239 | m = END_TRIPLE_QUOTE.match(line) 240 | if m: 241 | pydoc += m.group(1) 242 | entities.append((class_member or global_entity, pydoc)) 243 | pydoc = None 244 | else: 245 | pydoc += line 246 | continue 247 | 248 | m = BEGIN_TRIPLE_QUOTE.match(line) 249 | if m: 250 | pydoc = m.group(1) 251 | if pydoc.endswith('"""'): 252 | pydoc = pydoc[:-3] 253 | entities.append((class_member or global_entity, pydoc)) 254 | pydoc = None 255 | else: 256 | pydoc += "\n" 257 | continue 258 | 259 | if class_member and class_member.func_decl: 260 | func = class_member 261 | elif global_entity and global_entity.func_decl: 262 | func = global_entity 263 | else: 264 | func = None 265 | 266 | if func and func.func_decl and not func.func_decl.endswith(":"): 267 | if not func.func_decl.endswith("("): 268 | func.func_decl += " " 269 | func.func_decl += line.strip() 270 | if func.func_decl.endswith(":"): 271 | continue 272 | 273 | m = FUNCTION_RE.match(line) 274 | if m: 275 | class_member = None 276 | global_entity = GlobalEntity(name=m.group(1), kind=GlobalEntityType.FUNCTION) 277 | func = global_entity 278 | func.func_decl = m.group(1) + m.group(2).replace(", _as_task=False", "") 279 | continue 280 | 281 | m = CLASS_RE.match(line) 282 | if m: 283 | class_member = None 284 | global_entity = GlobalEntity(name=m.group(1), kind=GlobalEntityType.CLASS) 285 | 286 | m = METHOD_DECORATION_RE.match(line) 287 | if m: 288 | method_decoration = m.group(1) 289 | continue 290 | 291 | m = METHOD_RE.match(line) 292 | if m: 293 | if global_entity is None or global_entity.kind != GlobalEntityType.CLASS: 294 | if global_entity.kind != GlobalEntityType.FUNCTION: 295 | print(f"ERROR: encountered method `{m.group(1)}` while not in {global_entity.kind}") 296 | continue 297 | class_member = ClassMember( 298 | classname=global_entity.name, name=m.group(1), kind=ClassMemberType.METHOD, 299 | decoration=method_decoration) 300 | func = class_member 301 | func.func_decl = m.group(1) + m.group(2) 302 | method_decoration = None 303 | continue 304 | 305 | m = GLOBAL_ASSIGNMENT_RE.match(line) 306 | if m: 307 | class_member = None 308 | global_entity = GlobalEntity(name=m.group(1), kind=GlobalEntityType.ASSIGNMENT) 309 | 310 | if global_entity is not None and global_entity.kind == GlobalEntityType.CLASS: 311 | m = CLASS_ASSIGNMENT_RE.match(line) 312 | if m: 313 | class_member = ClassMember( 314 | classname=global_entity.name, name=m.group(1), kind=ClassMemberType.ASSIGNMENT) 315 | 316 | return entities 317 | 318 | 319 | def print_markdown(entities: List[Tuple[CodeEntity, str]]): 320 | anchors: Dict[str, str] = {} 321 | for entity, _ in entities: 322 | if entity: 323 | name = entity.fullname() 324 | anchors[name] = name.replace(".", "").lower() 325 | 326 | for entity, pydoc in entities: 327 | process_pydoc(entity, pydoc, anchors) 328 | 329 | 330 | if __name__ == "__main__": 331 | entities = parse_code_entities() 332 | print_markdown(entities) 333 | -------------------------------------------------------------------------------- /tools/split_text_to_chat_width.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 4 | # SPDX-License-Identifier: MIT 5 | 6 | from typing import List 7 | import sys 8 | 9 | # Most characters are 5 dots wide with the exceptions below. 10 | # Values from: https://minecraft.fandom.com/wiki/Language#Font 11 | irregular_char_widths = { 12 | " ": 3, 13 | "!": 1, 14 | '"': 3, 15 | "'": 1, 16 | "(": 3, 17 | ")": 3, 18 | "*": 3, 19 | ",": 1, 20 | ".": 1, 21 | ":": 1, 22 | ";": 1, 23 | "<": 4, 24 | ">": 4, 25 | "@": 6, 26 | "I": 3, 27 | "[": 3, 28 | "]": 3, 29 | "`": 2, 30 | "f": 4, 31 | "i": 1, 32 | "k": 4, 33 | "l": 2, 34 | "t": 3, 35 | "{": 3, 36 | "|": 1, 37 | "}": 3, 38 | "~": 6, 39 | } 40 | 41 | def get_char_width(c: str) -> int: 42 | return irregular_char_widths.get(c, 5) 43 | 44 | 45 | def resplit(text: str) -> List[str]: 46 | """Re-splits the given text into lines that fit Minecraft's chat screen. 47 | 48 | The default chat screen width is 295 dots. 49 | 50 | Args: 51 | text: text to be re-split across line 52 | 53 | Returns: 54 | list of strings representing lines that fit Minecraft's chat screen 55 | """ 56 | line_limit = 295 57 | lines = [] 58 | words = text.split() 59 | line = "" 60 | dots_for_line = 0 61 | dots_for_word = 0 62 | for word in words: 63 | dots_for_word = 0 64 | for ch in word: 65 | dots_for_word += get_char_width(ch) 66 | dots_for_word += len(word) - 1 # Reserve dots for spaces. 67 | dots_for_space = get_char_width(" ") if line else 0 68 | if dots_for_line + dots_for_space + dots_for_word <= line_limit: 69 | if line: 70 | line += " " 71 | line += word 72 | dots_for_line += dots_for_space + dots_for_word 73 | else: 74 | lines.append(line) 75 | line = word 76 | dots_for_line = dots_for_word 77 | dots_for_word = 0 78 | if line: 79 | lines.append(line) 80 | return lines 81 | 82 | 83 | if __name__ == "__main__": 84 | print("\n".join(resplit(sys.stdin.read()))) 85 | -------------------------------------------------------------------------------- /tools/update_version_number.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Greg Christiana 4 | # SPDX-License-Identifier: MIT 5 | 6 | # Updates the Minescript version number across configs and sources. If 7 | # --fork_docs is specified, docs/README.md is forked to 8 | # docs/v/README.md; if --nofork_docs is specified, docs are not 9 | # forked. Pass -n for a dry run, i.e. print the commands that would run or the 10 | # old version strings that would be matched, but do not rewrite the version 11 | # number. 12 | # 13 | # This script must be run from the 'minescript' directory, and expects 'fabric' 14 | # and 'forge' subdirectories. 15 | # 16 | # Usage: 17 | # tools/update_version_number.sh --fork_docs|--nofork_docs|--fork_docs_only -n 18 | # 19 | # Example: 20 | # Update version number from 2.1 to 2.2 and fork docs from to docs/v2.1: 21 | # $ tools/update_version_number.sh 2.1 2.2 --fork_docs 22 | 23 | old_version=${1:?"Error: old version number required."} 24 | new_version=${2:?"Error: new version number required."} 25 | fork_docs_arg=${3:?"Error: must specify --fork_docs or --nofork_docs or --fork_docs_only"} 26 | 27 | # Discard the fixed-position args. 28 | shift 3 29 | 30 | fork_docs_only=0 31 | if [[ $fork_docs_arg = "--fork_docs_only" ]]; then 32 | fork_docs=1 33 | fork_docs_only=1 34 | elif [[ $fork_docs_arg = "--fork_docs" ]]; then 35 | fork_docs=1 36 | elif [[ $fork_docs_arg = "--nofork_docs" ]]; then 37 | fork_docs=0 38 | else 39 | echo "Required 3rd arg must be --fork_docs or --nofork_docs." >&2 40 | exit 1 41 | fi 42 | 43 | dry_run=0 44 | 45 | while (( "$#" )); do 46 | case $1 in 47 | -n) 48 | dry_run=1 49 | ;; 50 | *) 51 | echo "Unrecognized arg: $1" >&2 52 | exit 2 53 | ;; 54 | esac 55 | shift 56 | done 57 | 58 | old_version_re=$(echo $old_version |sed 's/\./\\./g') 59 | 60 | if [ "$(basename $(pwd))" != "minescript" ]; then 61 | echo "update_version_number.sh must be run from 'minescript' directory." >&2 62 | exit 3 63 | fi 64 | 65 | function check_subdir_exists { 66 | subdir="$1" 67 | if [ ! -d "$subdir" ]; then 68 | echo "update_version_number.sh cannot find '${subdir}' subdirectory." >&2 69 | exit 4 70 | fi 71 | } 72 | 73 | check_subdir_exists fabric 74 | check_subdir_exists forge 75 | check_subdir_exists docs 76 | 77 | if [ ! -e docs/README.md ]; then 78 | echo "Required file missing: docs/README.md" >&2 79 | exit 5 80 | fi 81 | 82 | if [ $fork_docs = 1 ]; then 83 | old_version_docs=docs/v${old_version} 84 | if [ $dry_run = 0 ]; then 85 | mkdir "$old_version_docs" || (echo "$old_version_docs already exists." >&2; exit 6) 86 | 87 | old_version_readme=$old_version_docs/README.md 88 | cp -p docs/README.md "$old_version_readme" 89 | else 90 | echo mkdir "$old_version_docs" || (echo "$old_version_docs already exists." >&2; exit 7) 91 | fi 92 | fi 93 | 94 | # Rewrite version in first line of docs/README.md. 95 | if [ $dry_run = 0 ]; then 96 | sed -i '' -e \ 97 | "s/^## Minescript v${old_version} docs$/## Minescript v${new_version} docs/" \ 98 | docs/README.md 99 | else 100 | grep "^## Minescript v${old_version} docs$" docs/README.md 101 | fi 102 | 103 | if [ $fork_docs_only = 0 ]; then 104 | if [ $dry_run = 0 ]; then 105 | sed -i '' -e "s/^version=${old_version_re}$/version=${new_version}/" gradle.properties 106 | else 107 | grep -H "$old_version_re" gradle.properties 108 | fi 109 | 110 | for x in $(tools/find_version_number.sh $old_version -l |grep '\.py$'); do 111 | if [ $dry_run = 0 ]; then 112 | sed -i '' -e "s/v${old_version_re} /v${new_version} /" $x 113 | else 114 | grep -H "v${old_version_re} " $x 115 | fi 116 | done 117 | fi 118 | --------------------------------------------------------------------------------