diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..9acb09ed --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,5 @@ +{ + "permissions": { + "allow": ["Bash(./gradlew build *)"] + } +} diff --git a/Box3JS-NeoForge-1.21.1/.gitattributes b/Box3JS-NeoForge-1.21.1/.gitattributes new file mode 100644 index 00000000..b7bbcc47 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/.gitattributes @@ -0,0 +1,5 @@ +# Disable autocrlf on generated files, they always generate with LF +# Add any extra files or paths here to make git stop saying they +# are changed when only line endings change. +src/generated/**/.cache/* text eol=lf +src/generated/**/*.json text eol=lf diff --git a/Box3JS-NeoForge-1.21.1/.gitignore b/Box3JS-NeoForge-1.21.1/.gitignore new file mode 100644 index 00000000..fee2f7b6 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/.gitignore @@ -0,0 +1,40 @@ +### Gradle ### +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/**/build/ + +### IntelliJ IDEA ### +.idea/ +*.iws +*.iml +*.ipr +out/ +!**/src/**/out/ + +.run/ + +### Eclipse ### +.apt_generated +.classpath +.eclipse/ +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/**/bin/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +### Minecraft Modding ### +run/ +!**/src/**/run/ +**/src/generated/**/.cache/ +repo/ +!**/src/**/repo/ diff --git a/Box3JS-NeoForge-1.21.1/META-INF/neoforge.mods.toml b/Box3JS-NeoForge-1.21.1/META-INF/neoforge.mods.toml new file mode 100644 index 00000000..526db5a3 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/META-INF/neoforge.mods.toml @@ -0,0 +1,95 @@ +# This is an example neoforge.mods.toml file. It contains the data relating to the loading mods. +# There are several mandatory fields (#mandatory), and many more that are optional (#optional). +# The overall format is standard TOML format, v0.5.0. +# Note that there are a couple of TOML lists in this file. +# Find more information on toml format here: https://github.com/toml-lang/toml + +# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml +modLoader="javafml" #mandatory + +# A version range to match for said mod loader - for regular FML @Mod it will be the FML version. This is currently 2. +loaderVersion="${loader_version_range}" #mandatory + +# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. +# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. +license="${mod_license}" + +# A URL to refer people to when problems occur with this mod +#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional + +# A list of mods - how many allowed here is determined by the individual mod loader +[[mods]] #mandatory + +# The modid of the mod +modId="${mod_id}" #mandatory + +# The version number of the mod +version="${mod_version}" #mandatory + +# A display name for the mod +displayName="${mod_name}" #mandatory + +# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforged.net/docs/misc/updatechecker/ +#updateJSONURL="https://change.me.example.invalid/updates.json" #optional + +# A URL for the "homepage" for this mod, displayed in the mod UI +#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional + +# A file name (in the root of the mod JAR) containing a logo for display +#logoFile="examplemod.png" #optional + +# A text field displayed in the mod UI +#credits="" #optional + +# The authors of the mod, displayed in the mod UI (optional) +authors="神岛实验室" + +# The description text for the mod (multi line!) (#mandatory) +description=''' +Box3JS 为 Minecraft 服务端引入 JavaScript 脚本运行时,支持约 100 项 Box3 API 与 90 余项 Minecraft 扩展 API。无需 Java 基础即可编写服务端脚本。 +''' + +# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. +#[[mixins]] +#config="${mod_id}.mixins.json" + +# The [[accessTransformers]] block allows you to declare where your AT file is. +# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg +#[[accessTransformers]] +#file="META-INF/accesstransformer.cfg" + +# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json + +# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. +[[dependencies.${mod_id}]] #optional + # the modid of the dependency + modId="neoforge" #mandatory + # The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). + # 'required' requires the mod to exist, 'optional' does not + # 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning + type="required" #mandatory + # Optional field describing why the dependency is required or why it is incompatible + # reason="..." + # The version range of the dependency + versionRange="[${neo_version},)" #mandatory + # An ordering relationship for the dependency. + # BEFORE - This mod is loaded BEFORE the dependency + # AFTER - This mod is loaded AFTER the dependency + ordering="NONE" + # Side this dependency is applied on - BOTH, CLIENT, or SERVER + side="BOTH" + +# Here's another dependency +[[dependencies.${mod_id}]] + modId="minecraft" + type="required" + # This version range declares a minimum of the current minecraft version up to but not including the next major version + versionRange="${minecraft_version_range}" + ordering="NONE" + side="BOTH" + +# Features are specific properties of the game environment, that you may want to declare you require. This example declares +# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't +# stop your mod loading on the server for example. +#[features.${mod_id}] +#openGLVersion="[3.2,)" diff --git a/Box3JS-NeoForge-1.21.1/README.md b/Box3JS-NeoForge-1.21.1/README.md new file mode 100644 index 00000000..22afc7a4 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/README.md @@ -0,0 +1,63 @@ +# Box3JS(神岛代码)-- Minecraft Mod + +> **测试版(Beta)** — 本项目处于早期测试阶段,API 可能变动,可能存在未发现的缺陷。欢迎反馈问题。 + +[简体中文](README.md) | [English](README_en.md) + +`Box3JS` 是一个 Minecraft 服务端模组,延续了神奇代码岛的代码风格。你无需编写 Java,只需使用 TypeScript 即可开发脚本。 + +## 特性 + +- **TypeScript 支持** — 项目模板内置 TS 类型声明,完整类型检查 +- **Box3 API 兼容** — 实现了 Box3 平台核心 API(World / Entity / Player / Voxels / Storage) +- **MC 扩展** — 90+ Minecraft 独有功能:记分板、Bossbar、队伍、世界边界、粒子、烟花、药水等 +- **热重载** — `/box3script watch` 重新加载,无需重启 +- **项目管理** — 多项目隔离,独立启用/禁用,重启自动执行 + +## 快速开始 + +在游戏中(需要 OP 权限,等级 ≥ 2): + +``` +/box3script create mygame +``` + +这会创建一个 TypeScript 脚手架项目: + +``` +config/box3/script/mygame/ +├── .gitignore +├── package.json ← npm 依赖(esbuild、Babel、TypeScript) +├── tsconfig.json +├── build.mjs ← 构建脚本(esbuild → Babel → Rhino) +├── types/ +│ └── globals.d.ts ← Box3JS 完整类型声明 +└── src/ + └── app.ts ← 入口(含 Hello World 示例) +``` + +然后构建: + +```bash +cd config/box3/script/mygame +npm install +npm run build # 输出 dist/app.js +``` + +回到游戏启用: + +``` +/box3script on mygame +``` + +## 可用 API + +[API 总览 →](docs/api/README.md) ([English](docs/api/README_en.md)) + +## 命令 + +[命令详细参考 →](docs/api/commands.md) ([English](docs/api/commands_en.md)) + +## 许可证 + +Apache License 2.0 diff --git a/Box3JS-NeoForge-1.21.1/README_en.md b/Box3JS-NeoForge-1.21.1/README_en.md new file mode 100644 index 00000000..d765696e --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/README_en.md @@ -0,0 +1,63 @@ +# Box3JS -- Minecraft Mod + +> **Beta** — This project is in early beta. APIs may change, and undiscovered issues may still exist. Feedback is welcome. + +[简体中文](README.md) | [English](README_en.md) + +`Box3JS` is a Minecraft server-side mod inspired by Box3 coding style. You don’t need to write Java — just use TypeScript to build scripts. + +## Features + +- **TypeScript Support** — The project template includes TS type declarations with full type checking +- **Box3 API Compatibility** — Implements core Box3 APIs (World / Entity / Player / Voxels / Storage) +- **Minecraft Extensions** — 90+ Minecraft-specific features: scoreboards, bossbars, teams, world border, particles, fireworks, potions, and more +- **Hot Reload** — Reload scripts with `/box3script watch` without restarting +- **Project Management** — Multi-project isolation, independent enable/disable, and auto-run on restart + +## Quick Start + +In-game (requires OP level ≥ 2): + +``` +/box3script create mygame +``` + +This creates a TypeScript scaffold project: + +``` +config/box3/script/mygame/ +├── .gitignore +├── package.json ← npm dependencies (esbuild, Babel, TypeScript) +├── tsconfig.json +├── build.mjs ← build script (esbuild → Babel → Rhino) +├── types/ +│ └── globals.d.ts ← full Box3JS type declarations +└── src/ + └── app.ts ← entry point (includes Hello World example) +``` + +Then build: + +```bash +cd config/box3/script/mygame +npm install +npm run build # outputs dist/app.js +``` + +Back in game and enable it: + +``` +/box3script on mygame +``` + +## Available APIs + +[API Overview →](docs/api/README.md) ([English](docs/api/README_en.md)) + +## Commands + +[Full Command Reference →](docs/api/commands.md) ([English](docs/api/commands_en.md)) + +## License + +Apache License 2.0 diff --git a/Box3JS-NeoForge-1.21.1/build.gradle b/Box3JS-NeoForge-1.21.1/build.gradle new file mode 100644 index 00000000..3ab64091 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/build.gradle @@ -0,0 +1,207 @@ +plugins { + id 'java-library' + id 'maven-publish' + id 'net.neoforged.moddev' version '2.0.141' + id 'idea' +} + +tasks.named('wrapper', Wrapper).configure { + // Define wrapper values here so as to not have to always do so when updating gradlew.properties. + // Switching this to Wrapper.DistributionType.ALL will download the full gradle sources that comes with + // documentation attached on cursor hover of gradle classes and methods. However, this comes with increased + // file size for Gradle. If you do switch this to ALL, run the Gradle wrapper task twice afterwards. + // (Verify by checking gradle/wrapper/gradle-wrapper.properties to see if distributionUrl now points to `-all`) + distributionType = Wrapper.DistributionType.BIN +} + +version = mod_version +group = mod_group_id + +sourceSets.main.resources { + // Include resources generated by data generators. + srcDir('src/generated/resources') + + // Exclude common development only resources from finalized outputs + exclude("**/*.bbmodel") // BlockBench project files + exclude("src/generated/**/.cache") // datagen cache files +} + +repositories { + // Add here additional repositories if required by some of the dependencies below. +} + +base { + archivesName = mod_id +} + +// Mojang ships Java 21 to end users in 1.21.1, so mods should target Java 21. +java.toolchain.languageVersion = JavaLanguageVersion.of(21) + +neoForge { + // Specify the version of NeoForge to use. + version = project.neo_version + + parchment { + mappingsVersion = project.parchment_mappings_version + minecraftVersion = project.parchment_minecraft_version + } + + // This line is optional. Access Transformers are automatically detected + // accessTransformers = project.files('src/main/resources/META-INF/accesstransformer.cfg') + + // Default run configurations. + // These can be tweaked, removed, or duplicated as needed. + runs { + client { + client() + + // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + server { + server() + programArgument '--nogui' + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + // This run config launches GameTestServer and runs all registered gametests, then exits. + // By default, the server will crash when no gametests are provided. + // The gametest system is also enabled by default for other run configs under the /test command. + gameTestServer { + type = "gameTestServer" + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + data { + data() + + // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it + // gameDirectory = project.file('run-data') + + // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. + programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() + } + + // applies to all the run configs above + configureEach { + // Recommended logging data for a userdev environment + // The markers can be added/remove as needed separated by commas. + // "SCAN": For mods scan. + // "REGISTRIES": For firing of registry events. + // "REGISTRYDUMP": For getting the contents of all registries. + systemProperty 'forge.logging.markers', 'REGISTRIES' + + // Recommended logging level for the console + // You can set various levels here. + // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels + logLevel = org.slf4j.event.Level.DEBUG + } + } + + mods { + // define mod <-> source bindings + // these are used to tell the game which sources are for which mod + // multi mod projects should define one per mod + "${mod_id}" { + sourceSet(sourceSets.main) + } + } +} + +// Sets up a dependency configuration called 'localRuntime'. +// This configuration should be used instead of 'runtimeOnly' to declare +// a dependency that will be present for runtime testing but that is +// "optional", meaning it will not be pulled by dependents of this mod. +configurations { + runtimeClasspath.extendsFrom localRuntime +} + +dependencies { + // Rhino JS engine for Box3 script execution — shaded into mod JAR + implementation 'org.mozilla:rhino:1.9.1' + + // Example optional mod dependency with JEI + // The JEI API is declared for compile time use, while the full JEI artifact is used at runtime + // compileOnly "mezz.jei:jei-${mc_version}-common-api:${jei_version}" + // compileOnly "mezz.jei:jei-${mc_version}-neoforge-api:${jei_version}" + // We add the full version to localRuntime, not runtimeOnly, so that we do not publish a dependency on it + // localRuntime "mezz.jei:jei-${mc_version}-neoforge:${jei_version}" + + // Example mod dependency using a mod jar from ./libs with a flat dir repository + // This maps to ./libs/coolmod-${mc_version}-${coolmod_version}.jar + // The group id is ignored when searching -- in this case, it is "blank" + // implementation "blank:coolmod-${mc_version}:${coolmod_version}" + + // Example mod dependency using a file as dependency + // implementation files("libs/coolmod-${mc_version}-${coolmod_version}.jar") + + // Example project dependency using a sister or child project: + // implementation project(":myproject") + + // For more info: + // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html + // http://www.gradle.org/docs/current/userguide/dependency_management.html +} + +// Unpack Rhino classes into the compiled output directory so both NeoForge's +// dev launcher (which loads class files directly) and the production JAR +// (which packs from this directory) can find them at runtime. +tasks.register('unpackRhino', Copy) { + from(configurations.runtimeClasspath.filter { it.name.contains('rhino') }.collect { zipTree(it) }) + into layout.buildDirectory.dir('classes/java/main') + exclude 'META-INF/**' + exclude 'module-info.class' + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} +tasks.named('classes').configure { dependsOn 'unpackRhino' } + +// This block of code expands all declared replace properties in the specified resource targets. +// A missing property will result in an error. Properties are expanded using ${} Groovy notation. +var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) { + var replaceProperties = [ + minecraft_version : minecraft_version, + minecraft_version_range: minecraft_version_range, + neo_version : neo_version, + loader_version_range : loader_version_range, + mod_id : mod_id, + mod_name : mod_name, + mod_license : mod_license, + mod_version : mod_version, + ] + inputs.properties replaceProperties + expand replaceProperties + from "src/main/templates" + into "build/generated/sources/modMetadata" +} +// Include the output of "generateModMetadata" as an input directory for the build +// this works with both building through Gradle and the IDE. +sourceSets.main.resources.srcDir generateModMetadata +// To avoid having to run "generateModMetadata" manually, make it run on every project reload +neoForge.ideSyncTask generateModMetadata + +// Example configuration to allow publishing using the maven-publish plugin +publishing { + publications { + register('mavenJava', MavenPublication) { + from components.java + } + } + repositories { + maven { + url "file://${project.projectDir}/repo" + } + } +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation +} + +// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. +idea { + module { + downloadSources = true + downloadJavadoc = true + } +} diff --git a/Box3JS-NeoForge-1.21.1/docs/api/README.md b/Box3JS-NeoForge-1.21.1/docs/api/README.md new file mode 100644 index 00000000..84b1bc21 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/README.md @@ -0,0 +1,82 @@ +# Box3JS API 参考 + +Box3JS 是一个 Minecraft 模组,允许用 JavaScript 编写服务端脚本。所有脚本运行在 `config/box3/script/<项目名>` 下。 + +## 快速开始 + +```js +// app.js — 最简示例 +world.onTick(() => { + // 每 tick 执行 (20 tick = 1 秒) +}); + +world.onChat((entity, message, tick) => { + var p = entity.player; + if (message === "!hello") { + p.directMessage("Hello, " + p.name + "!"); + } +}); + +console.log("脚本已加载"); +``` + +## 全局对象 + +| 对象 | 类型 | 说明 | +| ---------------- | ------- | -------------------------------------------------------------------------------- | +| `world` | ✅ Box3 | 世界控制,见 [world.md](world.md) | +| `entity` | ✅ Box3 | 实体包装(回调参数,或通过 `world.spawnEntity` 创建),见 [entity.md](entity.md) | +| `player` | ✅ Box3 | 玩家包装(通过 `entity.player` 获取),见 [player.md](player.md) | +| `voxels` | ✅ Box3 | 方块操作,见 [voxels.md](voxels.md) | +| `storage` | ✅ Box3 | 数据持久化,见 [storage.md](storage.md) | +| `console` | ⬆ MC | `console.log/debug/warn/error/assert/clear` | +| `require(id)` | ⬆ MC | CommonJS 模块导入,见下方模块说明 | +| `sleep(ms)` | ⬆ MC | 阻塞线程指定毫秒 | +| `GameVector3` | ✅ Box3 | 三维向量,见 [math.md](math.md) | +| `GameBounds3` | ✅ Box3 | 包围盒,见 [math.md](math.md) | +| `GameRGBColor` | ✅ Box3 | RGB 颜色,见 [math.md](math.md) | +| `GameRGBAColor` | ✅ Box3 | RGBA 颜色,见 [math.md](math.md) | +| `GameQuaternion` | ✅ Box3 | 四元数,见 [math.md](math.md) | + +## API 标注说明 + +| 标注 | 含义 | +| --------------- | ------------------------------------------- | +| ✅ **Box3 API** | 源自 Box3 平台,命名和语义与 Box3 保持一致 | +| ⬆ **MC 扩展** | 非 Box3 原有,利用 Minecraft 特性新增的 API | + +## 文档索引 + +| 文档 | 内容 | +| -------------------------- | ------------------------------------------------------- | +| [world.md](world.md) | 世界状态、事件、记分板、Bossbar、队伍、边界、粒子、烟花 | +| [entity.md](entity.md) | 实体属性、AI、装备、药水、寻路、标签 | +| [player.md](player.md) | 背包、消息、飞行、游戏模式、传送、命令 | +| [voxels.md](voxels.md) | 方块读写、区域填充、刷怪笼 | +| [storage.md](storage.md) | 数据持久化存储 | +| [math.md](math.md) | Vector3、Bounds3、Color、Quaternion | +| [commands.md](commands.md) | `/box3script` 命令参考 | + +## 文件模块 + +**TypeScript 构建管线:** + +`/box3script create` 创建的项目自带完整的 TS 构建环境。写入 `src/*.ts`,构建输出到 `dist/app.js`: + +``` +config/box3/script/mygame/ +├── package.json ← esbuild + Babel + @babel/preset-typescript +├── tsconfig.json +├── build.mjs ← Babel TS→JS → esbuild bundle → dist/ +├── types/ +│ └── globals.d.ts ← 完整 API 类型声明 (IDE 自动补全) +├── src/ +│ ├── app.ts ← 入口,require() 其他模块 +│ ├── state.ts ← 共享游戏状态 +│ ├── course.ts ← 赛道数据与建筑 +│ └── ... +└── dist/ + └── app.js ← 编译产物(模组实际加载此文件) +``` + +`npm run build` 或 `node build.mjs` 执行构建。`/box3script watch` 可开启文件监控自动热重载。 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/README_en.md b/Box3JS-NeoForge-1.21.1/docs/api/README_en.md new file mode 100644 index 00000000..1af01526 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/README_en.md @@ -0,0 +1,82 @@ +# Box3JS API Reference + +Box3JS is a Minecraft mod that lets you write server-side scripts in JavaScript. All scripts run under `config/box3/script/`. + +## Quick Start + +```js +// app.js — minimal example +world.onTick(() => { + // runs every tick (20 ticks = 1 second) +}); + +world.onChat((entity, message, tick) => { + var p = entity.player; + if (message === "!hello") { + p.directMessage("Hello, " + p.name + "!"); + } +}); + +console.log("Script loaded"); +``` + +## Global Objects + +| Object | Type | Description | +| ---------------- | ------- | ---------------------------------------------------------------------------------- | +| `world` | ✅ Box3 | World control, see [world.md](world.md) | +| `entity` | ✅ Box3 | Entity wrapper (from callbacks or `world.spawnEntity`), see [entity.md](entity.md) | +| `player` | ✅ Box3 | Player wrapper (via `entity.player`), see [player.md](player.md) | +| `voxels` | ✅ Box3 | Block operations, see [voxels.md](voxels.md) | +| `storage` | ✅ Box3 | Data persistence, see [storage.md](storage.md) | +| `console` | ⬆ MC | `console.log/debug/warn/error/assert/clear` | +| `require(id)` | ⬆ MC | CommonJS module import, see module section below | +| `sleep(ms)` | ⬆ MC | Block the thread for the given milliseconds | +| `GameVector3` | ✅ Box3 | 3D vector, see [math.md](math.md) | +| `GameBounds3` | ✅ Box3 | Bounding box, see [math.md](math.md) | +| `GameRGBColor` | ✅ Box3 | RGB color, see [math.md](math.md) | +| `GameRGBAColor` | ✅ Box3 | RGBA color, see [math.md](math.md) | +| `GameQuaternion` | ✅ Box3 | Quaternion, see [math.md](math.md) | + +## API Legend + +| Label | Meaning | +| ------------------ | ------------------------------------------------------------------ | +| ✅ **Box3 API** | Originates from the Box3 platform; naming and semantics match Box3 | +| ⬆ **MC Extension** | Not in original Box3; added using Minecraft-specific features | + +## Document Index + +| Document | Content | +| -------------------------- | ----------------------------------------------------------------------------- | +| [world.md](world.md) | World state, events, scoreboard, bossbar, teams, border, particles, fireworks | +| [entity.md](entity.md) | Entity properties, AI, equipment, potions, pathfinding, tags | +| [player.md](player.md) | Inventory, messaging, flight, gamemode, teleport, commands | +| [voxels.md](voxels.md) | Block read/write, region fill, spawner control | +| [storage.md](storage.md) | Persistent data storage | +| [math.md](math.md) | Vector3, Bounds3, Color, Quaternion | +| [commands.md](commands.md) | `/box3script` command reference | + +## File Modules + +**TypeScript build pipeline:** + +Projects created with `/box3script create` come with a complete TS build environment. Write in `src/*.ts`, build outputs to `dist/app.js`: + +``` +config/box3/script/mygame/ +├── package.json ← esbuild + Babel + @babel/preset-typescript +├── tsconfig.json +├── build.mjs ← Babel TS→JS → esbuild bundle → dist/ +├── types/ +│ └── globals.d.ts ← Full API type declarations (IDE autocomplete) +├── src/ +│ ├── app.ts ← Entry point, require() other modules +│ ├── state.ts ← Shared game state +│ ├── course.ts ← Course data & building +│ └── ... +└── dist/ + └── app.js ← Compiled output (what the mod actually loads) +``` + +Run `npm run build` or `node build.mjs` to build. Use `/box3script watch` to enable file watching for auto hot-reload. diff --git a/Box3JS-NeoForge-1.21.1/docs/api/commands.md b/Box3JS-NeoForge-1.21.1/docs/api/commands.md new file mode 100644 index 00000000..370c4555 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/commands.md @@ -0,0 +1,189 @@ +# /box3script 命令参考 + +所有命令需要 **OP 权限等级 2**(默认管理员权限)。所有 `` 参数均支持 **Tab 自动补全**。 + +## 命令列表 + +### `/box3script create ` + +创建新的 TypeScript 脚本项目。在 `config/box3/script//` 下生成完整的 TS 脚手架。创建后默认**禁用**。 + +``` +/box3script create mygame +``` + +生成的文件结构: + +``` +config/box3/script/ + └── mygame/ + ├── .gitignore + ├── package.json ← 依赖(esbuild、Babel、TypeScript) + ├── tsconfig.json + ├── build.mjs ← 构建脚本 + ├── types/ + │ └── globals.d.ts ← Box3JS 类型声明 + └── src/ + └── app.ts ← 入口(含 Hello World 示例) +``` + +创建后需要手动安装依赖和构建: + +```bash +cd config/box3/script/mygame +npm install +npm run build # 输出 dist/app.js +``` + +### `/box3script` + +直接输入不带参数,列出所有项目及启用/禁用/沙盒状态。 + +``` +/box3script +``` + +输出示例: + +``` +=== Projects === + [ON] [SANDBOX] colorzone + [ON] demo + [OFF] siege +``` + +### `/box3script on ` + +启用指定项目并**立即加载执行**。加载错误会直接反馈到聊天栏。 + +``` +/box3script on mygame +``` + +### `/box3script on all` + +一键启用所有项目。 + +``` +/box3script on all +``` + +### `/box3script off ` + +禁用指定项目。下次服务端重启时不再自动执行。 + +``` +/box3script off siege +``` + +### `/box3script off all` + +一键禁用所有项目。 + +``` +/box3script off all +``` + +### `/box3script reload` + +停止所有脚本,重新加载所有已启用项目的 `app.js`。加载错误会反馈到聊天栏。 + +``` +/box3script reload +``` + +### `/box3script reload ` + +重新加载指定项目(先停止再启动)。未启用的项目会自动设为启用后启动。 + +``` +/box3script reload mygame +``` + +### `/box3script watch` + +开启/关闭文件监控。开启后监控所有项目的 `dist/` 目录,`.js` 文件变化时自动热重载对应项目。 + +``` +/box3script watch # 切换 开/关 +/box3script watch on # 开启 +/box3script watch off # 关闭 +``` + +### `/box3script sandbox ` + +切换沙盒模式。开启后自动追踪该项目所有的方块修改、实体/玩家/世界状态变更。**沙盒持久化**——`/box3script stop` 和 `/box3script reload` 不会清除沙盒状态,仅手动再次执行此命令才会关闭沙盒并回滚全部修改。关闭时在聊天栏显示恢复摘要。 + +``` +/box3script sandbox mygame # 切换 开/关 +``` + +**追踪内容:** + +| 类别 | 追踪项 | +| ---- | -------------------------------------------------------------------------------------- | +| 方块 | `setVoxel`/`setVoxelId`/`fillVoxel` 修改(上限 500 万块) | +| 实体 | HP、AI、隐身、发光、无敌、着火、药水效果、标签、名称、装备、掉落率、属性 | +| 玩家 | 游戏模式、飞行能力、速度、跳跃力、经验、饱食度、物品栏、护甲、药水、位置、维度、重生点 | +| 世界 | 天气、时间、难度、游戏规则、世界边界 | + +典型工作流: + +``` +/box3script sandbox mygame # 开启沙盒 +/box3script on mygame # 加载脚本 +# ... 测试、观察结果 ... +/box3script stop mygame # 停止脚本,不改世界 +# ... 修改代码、npm run build ... +/box3script on mygame # 再次测试 +# ... 满意后关闭沙盒回滚 ... +/box3script sandbox mygame # 关闭沙盒 → 回滚 + 显示摘要 +``` + +> **注意:** 沙盒仅追踪通过脚本 API 修改的方块(`setVoxel`/`setVoxelId`/`fillVoxel`)。直接用镐子挖的方块不受影响。追踪上限 500 万块,达到 90% 时控制台日志警告。 + +### `/box3script stop` + +停止所有项目,清除全部回调、定时器和作用域。**已开启沙盒的项目会自动保留沙盒追踪状态**,不会被回滚。 + +``` +/box3script stop +``` + +### `/box3script stop ` + +停止指定项目,仅清除该项目的回调、定时器和作用域,**不影响其他正在运行的项目**。沙盒项目会保留追踪状态,不会回滚。 + +``` +/box3script stop siege +``` + +## 配置文件 + +启用/禁用状态保存在 `config/box3/scripts.json`: + +```json +{ + "mygame": true, + "siege": false, + "mygame": true +} +``` + +## 脚本目录结构 + +``` +config/box3/ + ├── scripts.json ← 项目开关配置 + ├── script/ ← 脚本目录 + │ ├── mygame/ + │ │ ├── package.json + │ │ ├── src/app.ts + │ │ └── dist/app.js ← 编译产物 + │ └── mygame/ + │ ├── package.json + │ ├── src/app.ts + │ └── dist/app.js + └── storage/ ← 存储数据目录 (storage API) + └── ... +``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/commands_en.md b/Box3JS-NeoForge-1.21.1/docs/api/commands_en.md new file mode 100644 index 00000000..905019c8 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/commands_en.md @@ -0,0 +1,189 @@ +# /box3script Command Reference + +All commands require **OP level 2** (default admin permission). All `` arguments support **Tab completion**. + +## Command List + +### `/box3script create ` + +Creates a new TypeScript script project. Generates a complete TS scaffold under `config/box3/script//`. Created projects are **disabled** by default. + +``` +/box3script create mygame +``` + +Generated file structure: + +``` +config/box3/script/ + └── mygame/ + ├── .gitignore + ├── package.json ← dependencies (esbuild, Babel, TypeScript) + ├── tsconfig.json + ├── build.mjs ← build script + ├── types/ + │ └── globals.d.ts ← Box3JS type declarations + └── src/ + └── app.ts ← entry point (with Hello World example) +``` + +After creation, manually install dependencies and build: + +```bash +cd config/box3/script/mygame +npm install +npm run build # outputs dist/app.js +``` + +### `/box3script` + +With no arguments, lists all projects and their enable/disable/sandbox status. + +``` +/box3script +``` + +Example output: + +``` +=== Projects === + [ON] [SANDBOX] colorzone + [ON] demo + [OFF] siege +``` + +### `/box3script on ` + +Enables the specified project and **immediately loads and executes** it. Load errors are reported in chat. + +``` +/box3script on mygame +``` + +### `/box3script on all` + +Enables all projects at once. + +``` +/box3script on all +``` + +### `/box3script off ` + +Disables the specified project. It won't auto-run on next server restart. + +``` +/box3script off siege +``` + +### `/box3script off all` + +Disables all projects at once. + +``` +/box3script off all +``` + +### `/box3script reload` + +Stops all scripts and reloads `app.js` for all enabled projects. Load errors are reported in chat. + +``` +/box3script reload +``` + +### `/box3script reload ` + +Reloads the specified project (stop then start). If the project was disabled, it gets auto-enabled before starting. + +``` +/box3script reload mygame +``` + +### `/box3script watch` + +Toggle file watching on/off. When enabled, monitors the `dist/` directory of all projects and auto hot-reloads when `.js` files change. + +``` +/box3script watch # toggle on/off +/box3script watch on # turn on +/box3script watch off # turn off +``` + +### `/box3script sandbox ` + +Toggle sandbox mode. When enabled, automatically tracks all block modifications, entity/player/world state changes made by the project. **Sandbox state is persistent** — `/box3script stop` and `/box3script reload` do NOT clear sandbox tracking. Only manually running this command again will disable sandbox and roll back all modifications. Rollback summary is displayed in chat. + +``` +/box3script sandbox mygame # toggle on/off +``` + +**Tracked content:** + +| Category | Tracked Items | +| -------- | -------------------------------------------------------------------------------------------------------------------- | +| Blocks | `setVoxel`/`setVoxelId`/`fillVoxel` modifications (max 5 million blocks) | +| Entities | HP, AI, invisibility, glowing, invulnerability, fire, potion effects, tags, name, equipment, drop rate, attributes | +| Players | Gamemode, flight ability, speed, jump power, XP, food, inventory, armor, potions, position, dimension, respawn point | +| World | Weather, time, difficulty, game rules, world border | + +Typical workflow: + +``` +/box3script sandbox mygame # enable sandbox +/box3script on mygame # load script +# ... test, observe results ... +/box3script stop mygame # stop script, world unchanged +# ... edit code, npm run build ... +/box3script on mygame # test again +# ... satisfied, roll back ... +/box3script sandbox mygame # disable sandbox → rollback + summary +``` + +> **Note:** Sandbox only tracks block modifications made through script APIs (`setVoxel`/`setVoxelId`/`fillVoxel`). Blocks mined with a pickaxe are unaffected. Tracking limit is 5 million blocks; console warns at 90%. + +### `/box3script stop` + +Stops all projects, clearing all callbacks, timers, and scopes. **Projects with sandbox enabled automatically retain their sandbox tracking state** and are not rolled back. + +``` +/box3script stop +``` + +### `/box3script stop ` + +Stops the specified project, clearing only that project's callbacks, timers, and scope — **other running projects are unaffected**. Sandboxed projects retain tracking state without rollback. + +``` +/box3script stop siege +``` + +## Configuration File + +Enable/disable state is saved in `config/box3/scripts.json`: + +```json +{ + "mygame": true, + "siege": false, + "mygame": true +} +``` + +## Script Directory Structure + +``` +config/box3/ + ├── scripts.json ← project enable/disable config + ├── script/ ← scripts directory + │ ├── mygame/ + │ │ ├── package.json + │ │ ├── src/app.ts + │ │ └── dist/app.js ← compiled output + │ └── mygame/ + │ ├── package.json + │ ├── src/app.ts + │ └── dist/app.js + └── storage/ ← storage data directory (storage API) + └── ... +``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/entity.md b/Box3JS-NeoForge-1.21.1/docs/api/entity.md new file mode 100644 index 00000000..d8c1eef9 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/entity.md @@ -0,0 +1,352 @@ +# entity — 实体 API + +`entity` 代表 Minecraft 世界中的任意实体(怪物、动物、掉落物、玩家)。通过 `world.spawnEntity()`、`world.querySelector()`、`world.entitiesInRadius()` 或事件回调参数获取。 + +通过 `entity.player` 可获取该实体对应的 `player` 对象(仅当是玩家时有效)。 + +## 基本属性 + +### entity.id + +✅ Box3 API | 只读。实体的 UUID 字符串。 + +### entity.isPlayer() + +✅ Box3 API | 返回 `true` 表示该实体是玩家。 + +### entity.entityType + +✅ Box3 API | 只读。返回实体的命名空间 ID 字符串。 + +```js +var all = world.querySelectorAll("*"); +for (var i = 0; i < all.length; i++) { + var e = all[i]; + console.log(e.id + " -> " + e.entityType + " -> isPlayer: " + e.isPlayer()); +} +``` + +## 位置与移动 + +### entity.position + +✅ Box3 API | 只读 `GameVector3`。注意:这是一个 LiveVec3,调用 `.set(x,y,z)` 可直接传送实体。 + +```js +var pos = entity.position; +console.log(pos.x, pos.y, pos.z); + +// 传送 +entity.position.set(0, 100, 0); +``` + +### entity.velocity + +✅ Box3 API | 只读 `GameVector3`。LiveVec3,`.set(x,y,z)` 可直接修改速度。 + +```js +entity.velocity.set(0, 1, 0); // 向上的速度 +``` + +### entity.bounds + +✅ Box3 API | 只读 `GameVector3`。实体的包围盒半尺寸(half-size)。 + +### entity.onGround + +⬆ MC 扩展 | 只读。实体是否在地面上。 + +```js +if (entity.onGround) { + // 在地面 +} +``` + +### entity.eyePosition + +⬆ MC 扩展 | 只读 `GameVector3`。实体视线高度位置。 + +```js +var eye = entity.eyePosition; +``` + +## 生命值 + +### entity.hp + +✅ Box3 API | 获取/设置当前生命值(仅 LivingEntity 有效)。 + +### entity.maxHp + +✅ Box3 API | 获取/设置最大生命值(仅 LivingEntity 有效)。 + +```js +var zombie = world.spawnEntity("minecraft:zombie", new GameVector3(0, 100, 0)); +zombie.maxHp = 100; +zombie.hp = 100; +``` + +### entity.hurt(amount) + +✅ Box3 API | 对实体造成 `amount` 点伤害(触发伤害事件)。 + +### entity.heal(amount) + +✅ Box3 API | 治疗实体 `amount` 点生命值(不超过 maxHp)。 + +```js +zombie.hurt(10); // 造成 10 点伤害 +zombie.heal(5); // 治疗 5 点 +``` + +### entity.invulnerable + +⬆ MC 扩展 | 获取/设置实体是否无敌。 + +```js +entity.invulnerable = true; // 不受伤害 +console.log(entity.invulnerable); +``` + +## 外观 + +### entity.meshInvisible + +✅ Box3 API | 控制实体是否不可见。 + +```js +entity.meshInvisible = true; // 隐身 +``` + +### entity.glowing + +⬆ MC 扩展 | 获取/设置发光效果(类似光灵箭效果)。 + +```js +entity.glowing = true; // 实体发光 +console.log(entity.glowing); +``` + +### entity.nameTag + +⬆ MC 扩展 | 获取/设置实体的自定义名称(头上显示的名字)。 + +```js +entity.nameTag = "§cBoss 怪物"; +console.log(entity.nameTag); +``` + +## 标签系统 + +全部 ✅ Box3 API。标签是附加在实体上的字符串标记,用于分类和查询。 + +### entity.addTag(tag) + +添加标签。 + +### entity.hasTag(tag) + +检查是否有指定标签。 + +### entity.removeTag(tag) + +移除标签。 + +```js +entity.addTag("boss"); +entity.addTag("red_team"); + +if (entity.hasTag("boss")) { + entity.maxHp = 200; +} + +// 通过标签查询 +var bosses = world.querySelectorAll(".boss"); +``` + +## 火焰 + +### entity.setFire(ticks) + +⬆ MC 扩展 | 点燃实体指定 tick 数。20 ticks = 1 秒。 + +### entity.clearFire() + +⬆ MC 扩展 | 扑灭实体火焰。 + +```js +entity.setFire(100); // 点燃 5 秒 +entity.clearFire(); // 立即扑灭 +``` + +## AI 与导航 + +### entity.setAI(enabled) + +⬆ MC 扩展 | 启用/禁用实体 AI(仅 Mob 有效)。禁用后实体不会移动或攻击。 + +```js +entity.setAI(false); // 冻结实体 +``` + +### entity.setTarget(entity) + +⬆ MC 扩展 | 设置怪物攻击目标(仅 Mob 有效)。 + +### entity.getTarget() + +⬆ MC 扩展 | 获取当前攻击目标,返回 `Box3JSEntity` 或 null。 + +### entity.clearTarget() + +⬆ MC 扩展 | 清除攻击目标。 + +```js +var boss = world.spawnEntity("minecraft:skeleton", new GameVector3(0, 100, 0)); +var target = world.querySelectorAll("*")[0]; +boss.setTarget(target); +``` + +### entity.navigateTo(x, y, z, speed) + +⬆ MC 扩展 | 让实体寻路到目标坐标(仅 PathfinderMob 有效)。 + +### entity.navigateTo(pos, speed) + +⬆ GameVector3 重载。 + +```js +entity.navigateTo(10, 100, 10, 1.0); // 以 1.0 速度走过去 +entity.navigateTo(target.position, 1.0); +``` + +### entity.lookAt(x, y, z) + +⬆ MC 扩展 | 实体面朝目标坐标。 + +### entity.lookAt(pos) + +⬆ GameVector3 重载。 + +```js +entity.lookAt(0, 100, -10); +entity.lookAt(target.position); +``` + +## 药水效果 + +全部 ⬆ MC 扩展。 + +### entity.addEffect(effectId, duration, amplifier) + +添加药水效果。`duration` 单位为 tick,`amplifier` 从 0 开始。 + +### entity.addEffect(effectId, duration, amplifier, hideParticles) + +添加效果并可选隐藏粒子。 + +```js +entity.addEffect("minecraft:speed", 600, 2); // 速度 III,30 秒 +entity.addEffect("minecraft:strength", 99999, 1, true); // 永久力量 II,不显示粒子 +entity.addEffect("minecraft:glowing", 200, 0); // 发光 10 秒 + +// 常用效果: +// minecraft:speed, minecraft:slowness, minecraft:strength +// minecraft:weakness, minecraft:regeneration, minecraft:poison +// minecraft:jump_boost, minecraft:slow_falling, minecraft:invisibility +// minecraft:glowing, minecraft:levitation, minecraft:fire_resistance +``` + +## 装备 + +全部 ⬆ MC 扩展。 + +### entity.setEquipment(slot, itemId) + +给生物穿戴装备。 + +**slot 值:** `"mainhand"`、`"offhand"`、`"head"`(头盔)、`"chest"`(胸甲)、`"legs"`(护腿)、`"feet"`(靴子) + +```js +entity.setEquipment("mainhand", "minecraft:diamond_sword"); +entity.setEquipment("head", "minecraft:iron_helmet"); +entity.setEquipment("chest", "minecraft:iron_chestplate"); +entity.setEquipment("feet", "minecraft:leather_boots"); +``` + +### entity.setDropChance(slot, chance) + +设置装备槽物品的掉落概率,0.0–1.0。`slot` 设为 `"all"` 可一次性设置所有槽位。 + +```js +entity.setDropChance("mainhand", 0.5); // 50% 概率掉落主手物品 +entity.setDropChance("all", 0); // 不掉落任何装备 +``` + +## 属性 + +全部 ⬆ MC 扩展。 + +### entity.getAttribute(attributeId) + +获取实体属性当前值。 + +### entity.setAttribute(attributeId, value) + +设置实体属性基值。 + +```js +var attack = entity.getAttribute("minecraft:generic.attack_damage"); +entity.setAttribute("minecraft:generic.attack_damage", 10); +entity.setAttribute("minecraft:generic.max_health", 100); +entity.setAttribute("minecraft:generic.movement_speed", 0.5); +entity.setAttribute("minecraft:generic.knockback_resistance", 1.0); +entity.setAttribute("minecraft:generic.armor", 10); +``` + +> 注意:`maxHp` / `walkSpeed` / `jumpPower` 等 Box3 便捷属性内部也使用这些 attribute,推荐优先使用便捷属性,仅当需要访问未封装的属性时才用 `setAttribute`。 + +## 生命周期 + +### entity.destroy() + +✅ Box3 API | 销毁实体。如果设置了 `onDestroy` 回调,会触发它。 + +### entity.remove() + +⬆ MC 扩展 | 直接移除实体,**不触发** `onDestroy` 回调。 + +### entity.setOnDestroy(handler) + +✅ Box3 API | 设置销毁回调。`handler` 接收一个参数 `(entity)`。 + +### entity.destroyed + +✅ Box3 API | 只读。实体是否已被移除。 + +### entity.setPersistent(v) + +⬆ MC 扩展 | 设为 `true` 时生物不会因远离玩家而自然消失(仅 Mob 有效)。 + +```js +var boss = world.spawnEntity( + "minecraft:wither_skeleton", + new GameVector3(0, 100, 0), +); +boss.setPersistent(true); // 不会消失 +boss.setOnDestroy((e) => { + world.say("Boss 被击败了!"); +}); +``` + +## 自定义属性 + +✅ Box3 API | 可以直接在 entity 上存储任意 JS 数据,存活期等于实体生命周期。 + +```js +entity.myCustomField = "hello"; +entity.spawnTick = world.currentTick(); +entity.killCount = 0; + +console.log(entity.myCustomField); +``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/entity_en.md b/Box3JS-NeoForge-1.21.1/docs/api/entity_en.md new file mode 100644 index 00000000..007e8147 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/entity_en.md @@ -0,0 +1,352 @@ +# entity — Entity API + +`entity` represents any entity in the Minecraft world (mobs, animals, items, players). Obtain via `world.spawnEntity()`, `world.querySelector()`, `world.entitiesInRadius()`, or event callback parameters. + +Use `entity.player` to get the corresponding `player` object (only valid if the entity is a player). + +## Basic Properties + +### entity.id + +✅ Box3 API | Read-only. The entity's UUID string. + +### entity.isPlayer() + +✅ Box3 API | Returns `true` if this entity is a player. + +### entity.entityType + +✅ Box3 API | Read-only. Returns the entity's namespaced ID string. + +```js +var all = world.querySelectorAll("*"); +for (var i = 0; i < all.length; i++) { + var e = all[i]; + console.log(e.id + " -> " + e.entityType + " -> isPlayer: " + e.isPlayer()); +} +``` + +## Position & Movement + +### entity.position + +✅ Box3 API | Read-only `GameVector3`. Note: this is a LiveVec3 — calling `.set(x,y,z)` directly teleports the entity. + +```js +var pos = entity.position; +console.log(pos.x, pos.y, pos.z); + +// Teleport +entity.position.set(0, 100, 0); +``` + +### entity.velocity + +✅ Box3 API | Read-only `GameVector3`. LiveVec3 — `.set(x,y,z)` directly modifies velocity. + +```js +entity.velocity.set(0, 1, 0); // upward velocity +``` + +### entity.bounds + +✅ Box3 API | Read-only `GameVector3`. The entity's bounding box half-size. + +### entity.onGround + +⬆ MC Extension | Read-only. Whether the entity is on the ground. + +```js +if (entity.onGround) { + // on ground +} +``` + +### entity.eyePosition + +⬆ MC Extension | Read-only `GameVector3`. The entity's eye-level position. + +```js +var eye = entity.eyePosition; +``` + +## Health + +### entity.hp + +✅ Box3 API | Get/set current health (LivingEntity only). + +### entity.maxHp + +✅ Box3 API | Get/set maximum health (LivingEntity only). + +```js +var zombie = world.spawnEntity("minecraft:zombie", new GameVector3(0, 100, 0)); +zombie.maxHp = 100; +zombie.hp = 100; +``` + +### entity.hurt(amount) + +✅ Box3 API | Deal `amount` damage to the entity (triggers damage event). + +### entity.heal(amount) + +✅ Box3 API | Heal the entity by `amount` (capped at maxHp). + +```js +zombie.hurt(10); // deal 10 damage +zombie.heal(5); // heal 5 +``` + +### entity.invulnerable + +⬆ MC Extension | Get/set whether the entity is invulnerable. + +```js +entity.invulnerable = true; // immune to damage +console.log(entity.invulnerable); +``` + +## Appearance + +### entity.meshInvisible + +✅ Box3 API | Controls entity invisibility. + +```js +entity.meshInvisible = true; // invisible +``` + +### entity.glowing + +⬆ MC Extension | Get/set glowing effect (like spectral arrow effect). + +```js +entity.glowing = true; // entity glows +console.log(entity.glowing); +``` + +### entity.nameTag + +⬆ MC Extension | Get/set the entity's custom name (displayed above head). + +```js +entity.nameTag = "§cBoss Monster"; +console.log(entity.nameTag); +``` + +## Tag System + +All ✅ Box3 API. Tags are string markers attached to entities for classification and querying. + +### entity.addTag(tag) + +Add a tag. + +### entity.hasTag(tag) + +Check if the entity has the given tag. + +### entity.removeTag(tag) + +Remove a tag. + +```js +entity.addTag("boss"); +entity.addTag("red_team"); + +if (entity.hasTag("boss")) { + entity.maxHp = 200; +} + +// Query by tag +var bosses = world.querySelectorAll(".boss"); +``` + +## Fire + +### entity.setFire(ticks) + +⬆ MC Extension | Set the entity on fire for the given number of ticks. 20 ticks = 1 second. + +### entity.clearFire() + +⬆ MC Extension | Extinguish the entity's fire. + +```js +entity.setFire(100); // ignite for 5 seconds +entity.clearFire(); // extinguish immediately +``` + +## AI & Navigation + +### entity.setAI(enabled) + +⬆ MC Extension | Enable/disable entity AI (Mob only). When disabled, the entity won't move or attack. + +```js +entity.setAI(false); // freeze entity +``` + +### entity.setTarget(entity) + +⬆ MC Extension | Set the mob's attack target (Mob only). + +### entity.getTarget() + +⬆ MC Extension | Get the current attack target, returns `Box3JSEntity` or null. + +### entity.clearTarget() + +⬆ MC Extension | Clear the attack target. + +```js +var boss = world.spawnEntity("minecraft:skeleton", new GameVector3(0, 100, 0)); +var target = world.querySelectorAll("*")[0]; +boss.setTarget(target); +``` + +### entity.navigateTo(x, y, z, speed) + +⬆ MC Extension | Make the entity pathfind to the target coordinates (PathfinderMob only). + +### entity.navigateTo(pos, speed) + +⬆ GameVector3 overload. + +```js +entity.navigateTo(10, 100, 10, 1.0); // walk to target at speed 1.0 +entity.navigateTo(target.position, 1.0); +``` + +### entity.lookAt(x, y, z) + +⬆ MC Extension | Make the entity face the target coordinates. + +### entity.lookAt(pos) + +⬆ GameVector3 overload. + +```js +entity.lookAt(0, 100, -10); +entity.lookAt(target.position); +``` + +## Potion Effects + +All ⬆ MC Extension. + +### entity.addEffect(effectId, duration, amplifier) + +Add a potion effect. `duration` in ticks, `amplifier` starts at 0. + +### entity.addEffect(effectId, duration, amplifier, hideParticles) + +Add effect with optional particle hiding. + +```js +entity.addEffect("minecraft:speed", 600, 2); // Speed III, 30 seconds +entity.addEffect("minecraft:strength", 99999, 1, true); // Permanent Strength II, no particles +entity.addEffect("minecraft:glowing", 200, 0); // Glowing 10 seconds + +// Common effects: +// minecraft:speed, minecraft:slowness, minecraft:strength +// minecraft:weakness, minecraft:regeneration, minecraft:poison +// minecraft:jump_boost, minecraft:slow_falling, minecraft:invisibility +// minecraft:glowing, minecraft:levitation, minecraft:fire_resistance +``` + +## Equipment + +All ⬆ MC Extension. + +### entity.setEquipment(slot, itemId) + +Equip a mob with gear. + +**Slot values:** `"mainhand"`, `"offhand"`, `"head"` (helmet), `"chest"` (chestplate), `"legs"` (leggings), `"feet"` (boots) + +```js +entity.setEquipment("mainhand", "minecraft:diamond_sword"); +entity.setEquipment("head", "minecraft:iron_helmet"); +entity.setEquipment("chest", "minecraft:iron_chestplate"); +entity.setEquipment("feet", "minecraft:leather_boots"); +``` + +### entity.setDropChance(slot, chance) + +Set the drop probability for equipment in a slot, 0.0–1.0. Use `slot = "all"` to set all slots at once. + +```js +entity.setDropChance("mainhand", 0.5); // 50% chance to drop mainhand item +entity.setDropChance("all", 0); // drop nothing +``` + +## Attributes + +All ⬆ MC Extension. + +### entity.getAttribute(attributeId) + +Get the current value of an entity attribute. + +### entity.setAttribute(attributeId, value) + +Set the base value of an entity attribute. + +```js +var attack = entity.getAttribute("minecraft:generic.attack_damage"); +entity.setAttribute("minecraft:generic.attack_damage", 10); +entity.setAttribute("minecraft:generic.max_health", 100); +entity.setAttribute("minecraft:generic.movement_speed", 0.5); +entity.setAttribute("minecraft:generic.knockback_resistance", 1.0); +entity.setAttribute("minecraft:generic.armor", 10); +``` + +> Note: Box3 convenience properties like `maxHp` / `walkSpeed` / `jumpPower` use these attributes internally. Prefer convenience properties; use `setAttribute` only for attributes without dedicated wrappers. + +## Lifecycle + +### entity.destroy() + +✅ Box3 API | Destroy the entity. Fires the `onDestroy` callback if set. + +### entity.remove() + +⬆ MC Extension | Directly remove the entity, **without** firing the `onDestroy` callback. + +### entity.setOnDestroy(handler) + +✅ Box3 API | Set a destroy callback. `handler` receives one argument `(entity)`. + +### entity.destroyed + +✅ Box3 API | Read-only. Whether the entity has been removed. + +### entity.setPersistent(v) + +⬆ MC Extension | When `true`, the mob won't despawn when far from players (Mob only). + +```js +var boss = world.spawnEntity( + "minecraft:wither_skeleton", + new GameVector3(0, 100, 0), +); +boss.setPersistent(true); // won't despawn +boss.setOnDestroy((e) => { + world.say("Boss defeated!"); +}); +``` + +## Custom Properties + +✅ Box3 API | You can store arbitrary JS data directly on an entity. The data lives as long as the entity. + +```js +entity.myCustomField = "hello"; +entity.spawnTick = world.currentTick(); +entity.killCount = 0; + +console.log(entity.myCustomField); +``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/math.md b/Box3JS-NeoForge-1.21.1/docs/api/math.md new file mode 100644 index 00000000..82c65e13 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/math.md @@ -0,0 +1,182 @@ +# 数学类型 + +全部 ✅ Box3 API。以下数据类型在 JS 中全局可用。 + +## GameVector3 + +三维向量,用于位置、方向、速度等。 + +### 构造 + +```js +var v = new GameVector3(0, 100, 0); // x, y, z +``` + +### 属性 + +```js +v.x = 10; // 读写 +v.y = 20; +v.z = 30; +``` + +### 方法 + +| 方法 | 返回值 | 说明 | +| ---------------- | ------------- | ------------------ | +| `v.set(x, y, z)` | `GameVector3` | 设置分量,返回自身 | +| `v.add(w)` | `GameVector3` | 加法,返回新向量 | +| `v.sub(w)` | `GameVector3` | 减法 | +| `v.scale(s)` | `GameVector3` | 标量乘 | +| `v.dot(w)` | `number` | 点积 | +| `v.mag()` | `number` | 向量长度 | +| `v.sqrMag()` | `number` | 长度平方(更快) | +| `v.normalize()` | `GameVector3` | 归一化,返回新向量 | +| `v.distance(w)` | `number` | 两点距离 | +| `v.lerp(w, t)` | `GameVector3` | 线性插值,t 0–1 | +| `v.equals(w)` | `boolean` | 分量相等比较 | + +### 静态方法 + +```js +var v = GameVector3.fromPolar(mag, phi, theta); // 球坐标 → 向量 +``` + +```js +var pos = new GameVector3(0, 100, 0); +var target = new GameVector3(10, 100, 10); + +// 计算两点距离 +var dist = pos.distance(target); // ~14.14 + +// 方向向量 +var dir = target.sub(pos).normalize(); + +// 传送 +entity.position.set(0, 100, 0); +``` + +## GameBounds3 + +轴对齐包围盒(AABB)。 + +### 构造 + +```js +var bounds = new GameBounds3( + new GameVector3(-1, 0, -1), // 下界 (lo) + new GameVector3(1, 2, 1), // 上界 (hi) +); +``` + +### 方法 + +| 方法 | 返回值 | 说明 | +| -------------------------- | --------- | -------------------- | +| `bounds.intersects(other)` | `boolean` | 与另一包围盒是否相交 | +| `bounds.contains(point)` | `boolean` | 点是否在包围盒内 | + +## GameRGBColor + +RGB 颜色,分量范围 0.0–1.0。 + +### 构造 + +```js +var red = new GameRGBColor(1, 0, 0); +var blue = new GameRGBColor(0, 0, 1); +var gray = new GameRGBColor(0.5, 0.5, 0.5); +``` + +### 属性 + +```js +color.r = 0.5; // 读写 +color.g = 0.8; +color.b = 0.2; +``` + +### 方法 + +| 方法 | 返回值 | 说明 | +| -------------- | -------------- | -------- | +| `c.lerp(d, t)` | `GameRGBColor` | 线性插值 | + +### 静态方法 + +```js +var randomColor = GameRGBColor.random(); // 随机颜色 +``` + +## GameRGBAColor + +带 Alpha 通道的颜色,分量范围 0.0–1.0。 + +### 构造 + +```js +var semiRed = new GameRGBAColor(1, 0, 0, 0.5); +``` + +### 方法 + +```js +var a = new GameRGBAColor(1, 0, 0, 1); +var b = new GameRGBAColor(0, 1, 0, 0.5); + +var c = a.add(b); // 分量加法 +var d = a.sub(b); // 分量减法 +var e = a.mul(b); // 分量乘法 +var f = a.div(b); // 分量除法 + +a.addEq(b); // 原地加法 (a += b) +a.subEq(b); // 原地减法 +a.mulEq(b); // 原地乘法 +a.divEq(b); // 原地除法 + +a.blendEq(b); // 混合 + +a.set(0.5, 0.5, 0.5, 1); // 设置分量 +var result = new GameRGBAColor(0, 0, 0, 0); +result.copy(a); // 浅拷贝 a +var clone = a.clone(); // 深拷贝 + +var lerped = a.lerp(b, 0.5); // 插值 +var eq = a.equals(b); // 比较 +``` + +## GameQuaternion + +四元数,用于 3D 旋转。 + +### 构造 + +```js +var q = new GameQuaternion(0, 0, 0, 1); // w, x, y, z +``` + +### 方法 + +| 方法 | 说明 | +| ------------------------------------------------- | ------------------------- | +| `q.set(w, x, y, z)` | 设置分量 | +| `q.copy(other)` | 浅拷贝 | +| `q.clone()` | 深拷贝 | +| `q.add(p)` / `q.sub(p)` / `q.mul(p)` / `q.div(p)` | 算术 | +| `q.inv()` | 逆四元数 | +| `q.dot(p)` | 点积 | +| `q.mag()` / `q.sqrMag()` | 模长 | +| `q.normalize()` | 归一化 | +| `q.slerp(p, t)` | 球面线性插值 | +| `q.angle(p)` | 与另一四元数的夹角 (弧度) | +| `q.getAxisAngle()` | 获取旋转轴和角度 | +| `q.rotateX(a)` / `q.rotateY(a)` / `q.rotateZ(a)` | 绕轴旋转 | +| `q.equals(p)` | 比较 | + +### 静态方法 + +```js +var q1 = GameQuaternion.fromAxisAngle(axis, angle); +var q2 = GameQuaternion.fromEuler(x, y, z); +var q3 = GameQuaternion.rotationBetween(fromVec, toVec); +``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/math_en.md b/Box3JS-NeoForge-1.21.1/docs/api/math_en.md new file mode 100644 index 00000000..a0cc04ef --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/math_en.md @@ -0,0 +1,182 @@ +# Math Types + +All ✅ Box3 API. The following data types are available globally in JS. + +## GameVector3 + +3D vector for positions, directions, velocities, etc. + +### Construction + +```js +var v = new GameVector3(0, 100, 0); // x, y, z +``` + +### Properties + +```js +v.x = 10; // read/write +v.y = 20; +v.z = 30; +``` + +### Methods + +| Method | Returns | Description | +| ---------------- | ------------- | ----------------------------- | +| `v.set(x, y, z)` | `GameVector3` | Set components, returns self | +| `v.add(w)` | `GameVector3` | Addition, returns new vector | +| `v.sub(w)` | `GameVector3` | Subtraction | +| `v.scale(s)` | `GameVector3` | Scalar multiplication | +| `v.dot(w)` | `number` | Dot product | +| `v.mag()` | `number` | Vector magnitude | +| `v.sqrMag()` | `number` | Squared magnitude (faster) | +| `v.normalize()` | `GameVector3` | Normalize, returns new vector | +| `v.distance(w)` | `number` | Distance between two points | +| `v.lerp(w, t)` | `GameVector3` | Linear interpolation, t 0–1 | +| `v.equals(w)` | `boolean` | Component-wise equality | + +### Static Methods + +```js +var v = GameVector3.fromPolar(mag, phi, theta); // spherical → vector +``` + +```js +var pos = new GameVector3(0, 100, 0); +var target = new GameVector3(10, 100, 10); + +// Distance between two points +var dist = pos.distance(target); // ~14.14 + +// Direction vector +var dir = target.sub(pos).normalize(); + +// Teleport +entity.position.set(0, 100, 0); +``` + +## GameBounds3 + +Axis-aligned bounding box (AABB). + +### Construction + +```js +var bounds = new GameBounds3( + new GameVector3(-1, 0, -1), // lower bound (lo) + new GameVector3(1, 2, 1), // upper bound (hi) +); +``` + +### Methods + +| Method | Returns | Description | +| -------------------------- | --------- | -------------------------------------------- | +| `bounds.intersects(other)` | `boolean` | Whether it intersects another bounding box | +| `bounds.contains(point)` | `boolean` | Whether the point is inside the bounding box | + +## GameRGBColor + +RGB color, components range 0.0–1.0. + +### Construction + +```js +var red = new GameRGBColor(1, 0, 0); +var blue = new GameRGBColor(0, 0, 1); +var gray = new GameRGBColor(0.5, 0.5, 0.5); +``` + +### Properties + +```js +color.r = 0.5; // read/write +color.g = 0.8; +color.b = 0.2; +``` + +### Methods + +| Method | Returns | Description | +| -------------- | -------------- | -------------------- | +| `c.lerp(d, t)` | `GameRGBColor` | Linear interpolation | + +### Static Methods + +```js +var randomColor = GameRGBColor.random(); // random color +``` + +## GameRGBAColor + +Color with alpha channel, components range 0.0–1.0. + +### Construction + +```js +var semiRed = new GameRGBAColor(1, 0, 0, 0.5); +``` + +### Methods + +```js +var a = new GameRGBAColor(1, 0, 0, 1); +var b = new GameRGBAColor(0, 1, 0, 0.5); + +var c = a.add(b); // component-wise addition +var d = a.sub(b); // component-wise subtraction +var e = a.mul(b); // component-wise multiplication +var f = a.div(b); // component-wise division + +a.addEq(b); // in-place addition (a += b) +a.subEq(b); // in-place subtraction +a.mulEq(b); // in-place multiplication +a.divEq(b); // in-place division + +a.blendEq(b); // blend + +a.set(0.5, 0.5, 0.5, 1); // set components +var result = new GameRGBAColor(0, 0, 0, 0); +result.copy(a); // shallow copy from a +var clone = a.clone(); // deep copy + +var lerped = a.lerp(b, 0.5); // interpolation +var eq = a.equals(b); // comparison +``` + +## GameQuaternion + +Quaternion for 3D rotations. + +### Construction + +```js +var q = new GameQuaternion(0, 0, 0, 1); // w, x, y, z +``` + +### Methods + +| Method | Description | +| ------------------------------------------------- | ------------------------------------- | +| `q.set(w, x, y, z)` | Set components | +| `q.copy(other)` | Shallow copy | +| `q.clone()` | Deep copy | +| `q.add(p)` / `q.sub(p)` / `q.mul(p)` / `q.div(p)` | Arithmetic | +| `q.inv()` | Inverse quaternion | +| `q.dot(p)` | Dot product | +| `q.mag()` / `q.sqrMag()` | Magnitude | +| `q.normalize()` | Normalize | +| `q.slerp(p, t)` | Spherical linear interpolation | +| `q.angle(p)` | Angle to another quaternion (radians) | +| `q.getAxisAngle()` | Get rotation axis and angle | +| `q.rotateX(a)` / `q.rotateY(a)` / `q.rotateZ(a)` | Rotate around axis | +| `q.equals(p)` | Comparison | + +### Static Methods + +```js +var q1 = GameQuaternion.fromAxisAngle(axis, angle); +var q2 = GameQuaternion.fromEuler(x, y, z); +var q3 = GameQuaternion.rotationBetween(fromVec, toVec); +``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/player.md b/Box3JS-NeoForge-1.21.1/docs/api/player.md new file mode 100644 index 00000000..6dd37417 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/player.md @@ -0,0 +1,429 @@ +# player — 玩家 API + +`player` 对象通过 `entity.player` 获取,代表登录的玩家。它包含 `entity` 的全部能力,并额外提供玩家专属功能:背包、经验、飞行、消息、传送等。 + +```js +world.onPlayerJoin((entity) => { + var p = entity.player; // p 即为 player 对象 + p.directMessage("欢迎回来, " + p.name + "!"); +}); +``` + +## 基本信息 + +### player.name + +✅ Box3 API | 只读。玩家名称。 + +### player.userId + +✅ Box3 API | 只读。玩家 UUID 字符串。 + +### player.getOpLevel() + +⬆ MC 扩展 | 获取/设置玩家管理员权限等级 (0-4)。0=普通玩家, 1=可绕过出生点保护, 2=可使用大部分命令, 3=可管理玩家, 4=最高权限。 + +```js +if (player.getOpLevel() >= 2) { + // 需要权限等级 2 的操作 +} +player.opLevel = 3; // 设置为 3 级权限 +``` + +## 外观 + +### player.invisible + +✅ Box3 API | 获取/设置玩家是否隐形。 + +### player.scale + +✅ Box3 API | 只读。玩家缩放值。 + +```js +player.invisible = true; // 隐形 +``` + +## 移动 + +全部 ✅ Box3 API。 + +### player.walkSpeed + +步行速度,对应 `MOVEMENT_SPEED` 属性。默认值约 0.1。 + +### player.runSpeed + +奔跑速度。`walkSpeed × 1.3` 的关系自动保持。 + +### player.jumpPower + +跳跃力度,对应 `JUMP_STRENGTH` 属性。 + +### player.moveState + +只读。当前移动状态:`"FLYING"`、`"SWIM"`、`"JUMP"`、`"FALL"`、`"GROUND"`。 + +### player.walkState + +只读。当前行走状态:`"CROUCH"`、`"RUN"`、`"WALK"`、`"NONE"`。 + +```js +player.walkSpeed = 0.2; // 加速 +player.jumpPower = 0.6; // 跳更高 + +world.onTick(() => { + if (player.walkState === "RUN") { + // 玩家在奔跑 + } +}); +``` + +## 飞行 + +### player.canFly + +✅ Box3 API | 获取/设置飞行权限(`mayfly`)。设为 `true` 后玩家按跳跃键起飞。 + +### player.flying + +✅ Box3 API | 获取/设置是否正在飞行(`flying`)。需要先设置 `canFly = true`。 + +### player.flySpeed + +✅ Box3 API | 飞行速度。 + +### player.disableFly + +✅ Box3 API | 设为 `true` 时立即停止飞行并禁用飞行权限。 + +### player.spectator + +✅ Box3 API | 只读。玩家是否处于旁观模式。 + +```js +// 允许飞行 +player.canFly = true; +player.flySpeed = 0.1; + +// 让玩家起飞 +player.flying = true; + +// 强制落地 +player.disableFly = true; +``` + +### player.collision + +⬆ MC 扩展 | 获取/设置团队内碰撞。设为 `false` 可防止多人推搡。底层修改团队的 `CollisionRule`。 + +```js +player.collision = false; // 禁用碰撞 +console.log(player.collision); // false +``` + +## 生命值 + +⬆ MC 扩展 | 获取/设置玩家血量。`ServerPlayer` 本身是 `LivingEntity`,直接操作健康值。 + +### player.hp + +获取/设置当前生命值。 + +### player.maxHp + +获取/设置最大生命值。 + +```js +// 设置职业血量 +player.maxHp = 40; // 战士 40 HP +player.hp = 40; // 满血 + +// 设置后若当前血量超过新最大值会自动截断 +player.maxHp = 20; +// player.hp 自动降到 20 封顶 +``` + +## 游戏模式 + +### player.gameMode + +✅ Box3 API | 获取/设置游戏模式。get 返回名称字符串,set 接受字符串或数字。 + +```js +player.gameMode = "creative"; // 创造模式 +player.gameMode = "survival"; // 生存模式 +player.gameMode = "adventure"; // 冒险模式 +player.gameMode = "spectator"; // 旁观模式 +// 或数字: 0=生存, 1=创造, 2=冒险, 3=旁观 +``` + +## 相机 + +全部 ✅ Box3 API。 + +### player.cameraMode + +获取/设置相机模式:`"FPS"`(第一人称)或 `"FOLLOW"`(跟随实体)。 + +### player.cameraEntity + +设置或获取跟随的实体对象(`Box3JSEntity`)。 + +### player.cameraPitch / player.cameraYaw + +相机的俯仰角和偏航角。 + +### player.facingDirection + +只读 `GameVector3`。玩家视线方向单位向量。 + +### player.cameraTarget + +只读 `GameVector3`。玩家视线前方 5 格的目标点。 + +### player.lookAt(x, y, z) + +⬆ MC 扩展 | 让玩家看向指定坐标。 + +### player.lookAt(pos) + +⬆ GameVector3 重载。 + +```js +player.lookAt(10, 100, 10); +player.lookAt(target.position); + +// 获取视线方向 +var dir = player.facingDirection; +var target = player.cameraTarget; +``` + +## 传送与重生 + +### player.teleport(pos) + +✅ Box3 API | 传送玩家到指定 `GameVector3` 坐标。 + +### player.setRespawnPoint(pos) + +✅ Box3 API | 设置玩家的重生点。 + +### player.respawn() + +✅ Box3 API | 强制玩家重生(仅死亡状态有效)。 + +### player.dimension + +⬆ MC 扩展 | 获取/设置玩家所在维度。set 可跨维度传送。 + +```js +player.teleport(new GameVector3(0, 100, 0)); +player.setRespawnPoint(new GameVector3(0, 100, 0)); + +// 跨维度传送 +player.dimension = "minecraft:the_nether"; +player.teleport(new GameVector3(0, 70, 0)); +``` + +## 踢出 + +### player.kick() + +✅ Box3 API | 踢出玩家,默认提示 "Kicked"。 + +### player.kick(reason) + +✅ Box3 API | 踢出玩家,自定义原因。 + +```js +player.kick("你已被移出游戏"); +``` + +## 消息 + +### player.directMessage(msg) + +✅ Box3 API | 向玩家发送聊天栏消息。 + +### player.actionBar(msg) + +✅ Box3 API | 向玩家发送快捷栏上方消息(Action Bar)。 + +### player.title(title, subtitle) + +✅ Box3 API | 向玩家发送屏幕标题。使用默认动画参数。 + +### player.title(title, subtitle, fadeIn, stay, fadeOut) + +⬆ MC 扩展 | 完全参数的标题。`fadeIn`/`stay`/`fadeOut` 单位均为 tick。 + +### player.dialog(config) + +✅ Box3 API | 弹出对话框。传入 `{content, options}` 配置,返回 `{index, value}`。目前 MC 中发送系统消息作为简化实现。 + +```js +var result = player.dialog({ + content: "选择你的道路", + options: ["战士", "法师", "弓箭手"], +}); +player.directMessage("你选择了: " + result.value); +``` + +### player.link(href) + +✅ Box3 API | 向玩家发送可点击链接。 + +### player.onChat(handler) + +✅ Box3 API | 为单个玩家注册聊天回调(更精细的控制,常用于对话树)。 + +```js +player.directMessage("你好!"); +player.actionBar("§e按 !help 查看帮助"); +player.title("§6§lBOSS战", "§7击败所有敌人", 10, 60, 10); +player.link("https://example.com"); + +// 对话树 +player.directMessage("输入你的选择: A 或 B"); +player.onChat((entity, msg, tick) => { + if (msg === "A") { + player.directMessage("你选择了 A"); + } +}); +``` + +## 经验与饱食度 + +### player.xp + +⬆ MC 扩展 | 获取/设置经验等级。 + +### player.addExperienceLevels(levels) + +⬆ MC 扩展 | 增加 `levels` 级经验。 + +### player.food + +⬆ MC 扩展 | 获取/设置饱食度(0–20)。 + +### player.saturation + +⬆ MC 扩展 | 获取/设置饱和度(0–20,浮点数)。 + +```js +player.xp = 10; // 设置 10 级 +player.addExperienceLevels(3); // 加 3 级 +player.food = 20; +player.saturation = 10; +``` + +## 背包 + +全部 ⬆ MC 扩展。 + +### player.giveItem(itemId, count) + +给予物品。 + +### player.clearInventory() + +清空背包。 + +### player.getHeldItem() + +获取主手物品,返回 `{id, count}`。空手返回 `{id: "minecraft:air", count: 0}`。 + +```js +player.giveItem("minecraft:diamond_sword", 1); +player.giveItem("minecraft:golden_apple", 5); +player.giveItem("minecraft:arrow", 64); + +var held = player.getHeldItem(); +console.log(held.id, held.count); // "minecraft:diamond_sword" 1 + +player.clearInventory(); +``` + +### player.giveEnchantedItem(itemId, count, enchants) + +给予附魔物品。`enchants` 是 `{附魔ID: 等级}` 对象。 + +```js +player.giveEnchantedItem("minecraft:diamond_sword", 1, { + "minecraft:sharpness": 5, + "minecraft:fire_aspect": 2, + "minecraft:unbreaking": 3, +}); + +player.giveEnchantedItem("minecraft:bow", 1, { + "minecraft:power": 5, + "minecraft:punch": 2, + "minecraft:infinity": 1, +}); +``` + +### player.giveNamedItem(itemId, count, name, lore) + +给予带自定义名称和描述的物品。`lore` 为字符串数组。 + +```js +player.giveNamedItem("minecraft:gold_ingot", 1, "§6§l跑酷金牌", [ + "§7天空跑酷锦标赛", + "§e完赛时间: 1:23.450", +]); + +player.giveNamedItem("minecraft:diamond_sword", 1, "§c§l烈焰之刃", [ + "§7绑定: 火焰", + "§e右键: 发射火球", +]); +``` + +## 药水效果 + +### player.addEffect(effectId, duration, amplifier) + +⬆ MC 扩展 | 添加药水效果。`duration` 为 tick,`amplifier` 从 0 开始。 + +### player.addEffect(effectId, duration, amplifier, hideParticles) + +⬆ MC 扩展 | 添加效果并可选隐藏粒子。 + +### player.clearEffects() + +⬆ MC 扩展 | 移除所有药水效果。 + +```js +player.addEffect("minecraft:speed", 600, 2); +player.addEffect("minecraft:jump_boost", 99999, 1, true); // 永久,无粒子 +player.clearEffects(); +``` + +## 音效与指令 + +### player.playSound(path, volume, pitch) + +⬆ MC 扩展 | 向该玩家播放音效。`path` 为命名空间 ID。 + +### player.runCommand(cmd) + +⬆ MC 扩展 | 以玩家身份执行命令。 + +```js +player.playSound("minecraft:block.note_block.pling", 0.8, 1.5); +player.runCommand("say hello"); +``` + +## Tab 列表 + +### player.setPlayerListName(name) + +⬆ MC 扩展 | 修改该玩家在 Tab 列表中显示的名字。 + +```js +player.setPlayerListName("§e[CP3] §f" + player.name); +player.setPlayerListName("§6★ §f" + player.name); + +// 重置为原名 +player.setPlayerListName(player.name); +``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/player_en.md b/Box3JS-NeoForge-1.21.1/docs/api/player_en.md new file mode 100644 index 00000000..431729b3 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/player_en.md @@ -0,0 +1,474 @@ +# player — Player API + +The `player` object is obtained via `entity.player` and represents a logged-in player. It includes all `entity` capabilities plus player-specific features: inventory, XP, flight, messaging, teleport, etc. + +```js +world.onPlayerJoin((entity) => { + var p = entity.player; // p is the player object + p.directMessage("Welcome back, " + p.name + "!"); +}); +``` + +## Basic Info + +### player.name + +✅ Box3 API | Read-only. Player name. + +### player.userId + +✅ Box3 API | Read-only. Player UUID string. + +### player.getOpLevel() + +⬆ MC Extension | Get/set the player's operator permission level (0–4). 0 = normal player, 1 = bypass spawn protection, 2 = most commands, 3 = manage players, 4 = full access. + +```js +if (player.getOpLevel() >= 2) { + // operations requiring permission level 2 +} +player.opLevel = 3; // set to level 3 +``` + +## Appearance + +### player.invisible + +✅ Box3 API | Get/set whether the player is invisible. + +### player.scale + +✅ Box3 API | Read-only. Player scale value. + +```js +player.invisible = true; // invisible +``` + +## Movement + +All ✅ Box3 API. + +### player.walkSpeed + +Walking speed, corresponds to `MOVEMENT_SPEED` attribute. Default ~0.1. + +### player.runSpeed + +Running speed. Automatically maintained as `walkSpeed × 1.3`. + +### player.jumpPower + +Jump strength, corresponds to `JUMP_STRENGTH` attribute. + +### player.moveState + +Read-only. Current movement state: `"FLYING"`, `"SWIM"`, `"JUMP"`, `"FALL"`, `"GROUND"`. + +### player.walkState + +Read-only. Current walking state: `"CROUCH"`, `"RUN"`, `"WALK"`, `"NONE"`. + +```js +player.walkSpeed = 0.2; // speed up +player.jumpPower = 0.6; // jump higher + +world.onTick(() => { + if (player.walkState === "RUN") { + // player is sprinting + } +}); +``` + +## Flight + +### player.canFly + +✅ Box3 API | Get/set flight permission (`mayfly`). When `true`, player can take off by pressing jump. + +### player.flying + +✅ Box3 API | Get/set whether currently flying (`flying`). Requires `canFly = true` first. + +### player.flySpeed + +✅ Box3 API | Flight speed. + +### player.disableFly + +✅ Box3 API | When set to `true`, immediately stops flight and disables flight permission. + +### player.spectator + +✅ Box3 API | Read-only. Whether the player is in spectator mode. + +```js +// Allow flight +player.canFly = true; +player.flySpeed = 0.1; + +// Make player take off +player.flying = true; + +// Force landing +player.disableFly = true; +``` + +### player.collision + +⬆ MC Extension | Get/set team collision. Set to `false` to prevent players pushing each other. Modifies the team's `CollisionRule` internally. + +```js +player.collision = false; // disable collision +console.log(player.collision); // false +``` + +## Health + +⬆ MC Extension | Get/set player health. `ServerPlayer` is a `LivingEntity`, so health is accessed directly. + +### player.hp + +Get/set current health. + +### player.maxHp + +Get/set maximum health. + +```js +// Set class-based health +player.maxHp = 40; // Warrior 40 HP +player.hp = 40; // full health + +// If current HP exceeds new max, it's auto-capped +player.maxHp = 20; +// player.hp is auto-clamped to 20 +``` + +> Typically set during `!join` or `!ready` phase for class-based HP. Use `player.addEffect("minecraft:instant_health", ...)` for healing afterward. + +## Gamemode + +### player.gameMode + +✅ Box3 API | Get/set the player's gamemode. Get returns the name string; set accepts a string or number. + +```js +player.gameMode = "creative"; // creative mode +player.gameMode = "survival"; // survival mode +player.gameMode = "adventure"; // adventure mode +player.gameMode = "spectator"; // spectator mode +// or numbers: 0=survival, 1=creative, 2=adventure, 3=spectator +``` + +## Camera + +All ✅ Box3 API. + +### player.cameraMode + +Get/set camera mode: `"FPS"` (first person) or `"FOLLOW"` (follow entity). + +### player.cameraEntity + +Set or get the followed entity object (`Box3JSEntity`). + +### player.cameraPitch / player.cameraYaw + +Camera pitch and yaw angles. + +### player.facingDirection + +Read-only `GameVector3`. The player's look direction unit vector. + +### player.cameraTarget + +Read-only `GameVector3`. The point 5 blocks ahead of the player's eyes. + +### player.lookAt(x, y, z) + +⬆ MC Extension | Make the player look at the given coordinates. + +### player.lookAt(pos) + +⬆ GameVector3 overload. + +```js +player.lookAt(10, 100, 10); +player.lookAt(target.position); + +// Get look direction +var dir = player.facingDirection; +var target = player.cameraTarget; +``` + +## Teleport & Respawn + +### player.teleport(pos) + +✅ Box3 API | Teleport the player to the given `GameVector3` coordinates. + +### player.setRespawnPoint(pos) + +✅ Box3 API | Set the player's respawn point. + +### player.respawn() + +✅ Box3 API | Force the player to respawn (only effective when dead). + +### player.dimension + +⬆ MC Extension | Get/set the player's dimension. Set can teleport cross-dimension. + +```js +player.teleport(new GameVector3(0, 100, 0)); +player.setRespawnPoint(new GameVector3(0, 100, 0)); + +// Cross-dimension teleport +player.dimension = "minecraft:the_nether"; +player.teleport(new GameVector3(0, 70, 0)); +``` + +## Kick + +### player.kick() + +✅ Box3 API | Kick the player, default reason "Kicked". + +### player.kick(reason) + +✅ Box3 API | Kick the player with a custom reason. + +```js +player.kick("You have been removed from the game"); +``` + +## Messaging + +### player.directMessage(msg) + +✅ Box3 API | Send a chat message to the player. + +### player.actionBar(msg) + +✅ Box3 API | Send an action bar message (above the hotbar). + +### player.title(title, subtitle) + +✅ Box3 API | Send a screen title to the player. Uses default animation parameters. + +### player.title(title, subtitle, fadeIn, stay, fadeOut) + +⬆ MC Extension | Full-parameter title. `fadeIn`/`stay`/`fadeOut` are in ticks. + +### player.dialog(config) + +✅ Box3 API | Show a dialog. Pass `{content, options}` config, returns `{index, value}`. Currently implemented as a simplified system message in MC. + +```js +var result = player.dialog({ + content: "Choose your path", + options: ["Warrior", "Mage", "Archer"], +}); +player.directMessage("You chose: " + result.value); +``` + +### player.link(href) + +✅ Box3 API | Send a clickable link to the player. + +### player.onChat(handler) + +✅ Box3 API | Register a per-player chat callback (for finer control, commonly used in dialogue trees). + +```js +player.directMessage("Hello!"); +player.actionBar("§eType !help for help"); +player.title("§6§lBOSS FIGHT", "§7Defeat all enemies", 10, 60, 10); +player.link("https://example.com"); + +// Dialogue tree +player.directMessage("Enter your choice: A or B"); +player.onChat((entity, msg, tick) => { + if (msg === "A") { + player.directMessage("You chose A"); + } +}); +``` + +## XP & Food + +### player.xp + +⬆ MC Extension | Get/set experience level. + +### player.addExperienceLevels(levels) + +⬆ MC Extension | Add `levels` experience levels. + +### player.food + +⬆ MC Extension | Get/set food level (0–20). + +### player.saturation + +⬆ MC Extension | Get/set saturation (0–20, float). + +```js +player.xp = 10; // set to level 10 +player.addExperienceLevels(3); // add 3 levels +player.food = 20; +player.saturation = 10; +``` + +## Inventory + +All ⬆ MC Extension. + +### player.giveItem(itemId, count) + +Give an item. + +### player.clearInventory() + +Clear the inventory. + +### player.getHeldItem() + +Get the main hand item, returns `{id, count}`. Empty hand returns `{id: "minecraft:air", count: 0}`. + +```js +player.giveItem("minecraft:diamond_sword", 1); +player.giveItem("minecraft:golden_apple", 5); +player.giveItem("minecraft:arrow", 64); + +var held = player.getHeldItem(); +console.log(held.id, held.count); // "minecraft:diamond_sword" 1 + +player.clearInventory(); +``` + +### player.giveEnchantedItem(itemId, count, enchants) + +Give an enchanted item. `enchants` is a `{enchantmentId: level}` object. + +```js +player.giveEnchantedItem("minecraft:diamond_sword", 1, { + "minecraft:sharpness": 5, + "minecraft:fire_aspect": 2, + "minecraft:unbreaking": 3, +}); + +player.giveEnchantedItem("minecraft:bow", 1, { + "minecraft:power": 5, + "minecraft:punch": 2, + "minecraft:infinity": 1, +}); +``` + +### player.giveNamedItem(itemId, count, name, lore) + +Give an item with a custom name and lore. `lore` is a string array. + +```js +player.giveNamedItem("minecraft:gold_ingot", 1, "§6§lParkour Gold Medal", [ + "§7Sky Parkour Championship", + "§eFinish time: 1:23.450", +]); + +player.giveNamedItem("minecraft:diamond_sword", 1, "§c§lBlade of Flame", [ + "§7Bound: Fire", + "§eRight-click: Launch fireball", +]); +``` + +## Potion Effects + +### player.addEffect(effectId, duration, amplifier) + +⬆ MC Extension | Add a potion effect. `duration` in ticks, `amplifier` starts at 0. + +### player.addEffect(effectId, duration, amplifier, hideParticles) + +⬆ MC Extension | Add effect with optional particle hiding. + +### player.clearEffects() + +⬆ MC Extension | Remove all potion effects. + +```js +player.addEffect("minecraft:speed", 600, 2); +player.addEffect("minecraft:jump_boost", 99999, 1, true); // permanent, no particles +player.clearEffects(); +``` + +## Sound & Commands + +### player.playSound(path, volume, pitch) + +⬆ MC Extension | Play a sound to this player. `path` is a namespaced ID. + +### player.runCommand(cmd) + +⬆ MC Extension | Execute a command as this player. + +```js +player.playSound("minecraft:block.note_block.pling", 0.8, 1.5); +player.runCommand("say hello"); +``` + +## Tab List + +### player.setPlayerListName(name) + +⬆ MC Extension | Modify this player's displayed name in the tab list. + +```js +player.setPlayerListName("§e[CP3] §f" + player.name); +player.setPlayerListName("§6★ §f" + player.name); + +// Reset to original name +player.setPlayerListName(player.name); +``` + +## Box3 API List + +| API | Type | +| ----------------------------------------------------------- | ------- | +| `name` | ✅ Box3 | +| `userId` | ✅ Box3 | +| `invisible` | ✅ Box3 | +| `scale` | ✅ Box3 | +| `walkSpeed` / `runSpeed` / `jumpPower` | ✅ Box3 | +| `moveState` / `walkState` | ✅ Box3 | +| `canFly` / `flying` / `flySpeed` / `disableFly` | ✅ Box3 | +| `spectator` | ✅ Box3 | +| `gameMode` | ✅ Box3 | +| `cameraMode` / `cameraEntity` / `cameraPitch` / `cameraYaw` | ✅ Box3 | +| `facingDirection` / `cameraTarget` | ✅ Box3 | +| `setRespawnPoint()` / `respawn()` | ✅ Box3 | +| `kick()` | ✅ Box3 | +| `teleport()` | ✅ Box3 | +| `directMessage()` / `actionBar()` | ✅ Box3 | +| `title()` (2-param) | ✅ Box3 | +| `dialog()` | ✅ Box3 | +| `link()` | ✅ Box3 | +| `onChat()` (player-level) | ✅ Box3 | + +## MC Extension List + +| API | Type | +| ---------------------------------------------------- | ---- | +| `collision` | ⬆ MC | +| `title()` (5-param) | ⬆ MC | +| `hp` / `maxHp` | ⬆ MC | +| `xp` / `addExperienceLevels()` | ⬆ MC | +| `food` / `saturation` | ⬆ MC | +| `giveItem()` / `clearInventory()` / `getHeldItem()` | ⬆ MC | +| `giveEnchantedItem()` / `giveNamedItem()` | ⬆ MC | +| `addEffect()` (3/4-param) / `clearEffects()` | ⬆ MC | +| `playSound()` | ⬆ MC | +| `dimension` | ⬆ MC | +| `lookAt()` | ⬆ MC | +| `runCommand()` | ⬆ MC | +| `setPlayerListName()` | ⬆ MC | +| `getOpLevel()` / `opLevel` | ⬆ MC | diff --git a/Box3JS-NeoForge-1.21.1/docs/api/storage.md b/Box3JS-NeoForge-1.21.1/docs/api/storage.md new file mode 100644 index 00000000..00c58cce --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/storage.md @@ -0,0 +1,171 @@ +# storage — 数据存储 API + +`storage` 提供 JSON 文件持久化存储,带内存缓存加速读写。数据保存在 `config/box3/storage/<项目名>/` 目录下,每个项目自动拥有独立命名空间。 + +## 获取存储实例 + +### storage.getDataStorage(name) + +✅ Box3 API | 获取或创建一个命名存储。同名存储返回同一实例。 + +### storage.getGroupStorage(name) + +✅ Box3 API | 获取**跨项目共享**存储。所有项目通过同一 `name` 访问同一份数据(底层使用 `__shared__/` 命名空间)。适合做全服排行榜、全局配置等。 + +```js +var store = storage.getDataStorage("leaderboard"); +var config = storage.getDataStorage("settings"); +``` + +## 读写操作 + +### store.set(key, value) + +✅ Box3 API | 存储键值对。`value` 可以是字符串、数字、对象(自动 JSON 序列化)。 + +### store.get(key) + +✅ Box3 API | 获取值。返回存储时的原始类型。 + +```js +store.set("highScore", 100); +store.set("lastWinner", "Steve"); +store.set("config", { difficulty: "hard", maxPlayers: 10 }); + +var score = store.get("highScore"); // 100 (number) +var winner = store.get("lastWinner"); // "Steve" (string) +var cfg = store.get("config"); // {difficulty: "hard", ...} (object — 需要 JSON.parse) +``` + +> **注意:** 存储对象时,`store.get()` 返回 JSON 字符串,需要手动 `JSON.parse()`: +> +> ```js +> var cfg = JSON.parse(store.get("config")); +> console.log(cfg.difficulty); // "hard" +> ``` + +### store.keys() + +✅ Box3 API | 返回所有 key 的数组。 + +```js +var keys = store.keys(); +for (var i = 0; i < keys.length; i++) { + console.log(keys[i] + " = " + store.get(keys[i])); +} +``` + +## 更新与删除 + +### store.update(key, handler) + +✅ Box3 API | 回调式更新值。`handler` 接收当前值,返回新值。类似于 `store.set(key, handler(store.get(key)))`,但保证原子性。 + +```js +store.set("counter", 0); +store.update("counter", function (current) { + return current + 1; // 原子递增 +}); +``` + +### store.remove(key) + +✅ Box3 API | 删除指定 key。 + +### store.destroy() + +✅ Box3 API | 删除整个存储文件(同时清除内存缓存)。 + +```js +store.remove("tempKey"); +store.destroy(); // 删除该存储的所有数据 +``` + +## 数值操作 + +### store.increment(key, delta) + +✅ Box3 API | 递增数值。`delta` 默认为 1。 + +```js +store.set("kills", 0); +store.increment("kills"); // kills = 1 +store.increment("kills", 5); // kills = 6 +store.increment("kills", -2); // kills = 4 +``` + +## 分页查询 + +### store.list(options) + +✅ Box3 API | 游标分页查询。`options` 对象支持的字段: + +| 字段 | 类型 | 说明 | +| ------------------ | ------- | ----------------------------------- | +| `cursor` | number | 起始游标(页码 × pageSize) | +| `pageSize` | number | 每页条目数(1–100,默认 100) | +| `ascending` | boolean | 是否升序排列 | +| `max` | number | 值的上限过滤 | +| `min` | number | 值的下限过滤 | +| `constraintTarget` | string | 排序/过滤的嵌套路径(如 `"a.b.c"`) | + +返回 `QueryList` 分页对象: + +| 属性/方法 | 说明 | +| ------------------------- | ------------------ | +| `result.isLastPage` | 是否最后一页 | +| `result.getCurrentPage()` | 返回当前页条目数组 | +| `result.nextPage()` | 移到下一页 | + +每条条目为 `{key, value, updateTime, createTime, version}`。 + +```js +var result = store.list({ pageSize: 10, ascending: false }); + +// 遍历当前页 +var page = result.getCurrentPage(); +for (var i = 0; i < page.length; i++) { + console.log(page[i].key + ": " + page[i].value); +} + +// 下一页 +if (!result.isLastPage) { + result.nextPage(); +} +``` + +## 内存缓存与持久化 + +所有 `GameDataStorage` 实例共享一个内存缓存(`ConcurrentHashMap`)。首次访问时从磁盘加载 JSON,后续读写均在内存中操作,每次写操作(`set`/`update`/`remove`/`increment`)同步刷盘。 + +- **同名存储**:同一文件路径多次 `getDataStorage` 返回共享同一份内存数据,避免重复 I/O +- **项目隔离**:`getDataStorage("scores")` 在不同项目中访问不同文件(自动添加项目名前缀) +- **跨项目共享**:`getGroupStorage("leaderboard")` 所有项目访问同一个 `__shared__/leaderboard.json` + +## 完整示例:排行榜 + +```js +// 跨项目共享排行榜 — 所有项目读写同一份数据 +var lb = storage.getGroupStorage("leaderboard"); + +// 保存成绩 +function saveScore(name, time) { + lb.set(name, time); +} + +saveScore("Steve", 12345); +saveScore("Alex", 9800); + +// 遍历所有条目 +var result = lb.list({ pageSize: 10, ascending: true }); +while (true) { + var page = result.getCurrentPage(); + for (var i = 0; i < page.length; i++) { + console.log(page[i].key + ": " + page[i].value); + } + if (result.isLastPage) break; + result.nextPage(); +} +``` + +全部 ✅ Box3 API。 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md b/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md new file mode 100644 index 00000000..891fa38c --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md @@ -0,0 +1,171 @@ +# storage — Data Storage API + +`storage` provides JSON file persistence with in-memory caching for fast reads/writes. Data is saved under `config/box3/storage//`. Each project automatically gets an independent namespace. + +## Getting a Storage Instance + +### storage.getDataStorage(name) + +✅ Box3 API | Gets or creates a named storage. Same name returns the same instance. + +### storage.getGroupStorage(name) + +✅ Box3 API | Gets a **cross-project shared** storage. All projects access the same data via the same `name` (uses `__shared__/` namespace internally). Useful for global leaderboards, shared config, etc. + +```js +var store = storage.getDataStorage("leaderboard"); +var config = storage.getDataStorage("settings"); +``` + +## Read & Write + +### store.set(key, value) + +✅ Box3 API | Store a key-value pair. `value` can be a string, number, or object (auto JSON-serialized). + +### store.get(key) + +✅ Box3 API | Get a value. Returns the original type. + +```js +store.set("highScore", 100); +store.set("lastWinner", "Steve"); +store.set("config", { difficulty: "hard", maxPlayers: 10 }); + +var score = store.get("highScore"); // 100 (number) +var winner = store.get("lastWinner"); // "Steve" (string) +var cfg = store.get("config"); // "{difficulty:\"hard\",...}" (JSON string) +``` + +> **Note:** When storing objects, `store.get()` returns a JSON string. Use `JSON.parse()`: +> +> ```js +> var cfg = JSON.parse(store.get("config")); +> console.log(cfg.difficulty); // "hard" +> ``` + +### store.keys() + +✅ Box3 API | Returns an array of all keys. + +```js +var keys = store.keys(); +for (var i = 0; i < keys.length; i++) { + console.log(keys[i] + " = " + store.get(keys[i])); +} +``` + +## Update & Delete + +### store.update(key, handler) + +✅ Box3 API | Callback-based value update. `handler` receives the current value and returns the new value. Equivalent to `store.set(key, handler(store.get(key)))` but guarantees atomicity. + +```js +store.set("counter", 0); +store.update("counter", function (current) { + return current + 1; // atomic increment +}); +``` + +### store.remove(key) + +✅ Box3 API | Deletes the specified key. + +### store.destroy() + +✅ Box3 API | Deletes the entire storage file (also clears the in-memory cache). + +```js +store.remove("tempKey"); +store.destroy(); // delete all data in this storage +``` + +## Numeric Operations + +### store.increment(key, delta) + +✅ Box3 API | Increment a numeric value. `delta` defaults to 1. + +```js +store.set("kills", 0); +store.increment("kills"); // kills = 1 +store.increment("kills", 5); // kills = 6 +store.increment("kills", -2); // kills = 4 +``` + +## Paginated Queries + +### store.list(options) + +✅ Box3 API | Cursor-based paginated query. Supported `options` fields: + +| Field | Type | Description | +| ------------------ | ------- | -------------------------------------------------- | +| `cursor` | number | Starting cursor (page × pageSize) | +| `pageSize` | number | Entries per page (1–100, default 100) | +| `ascending` | boolean | Sort ascending | +| `max` | number | Upper value filter | +| `min` | number | Lower value filter | +| `constraintTarget` | string | Nested path for sorting/filtering (e.g. `"a.b.c"`) | + +Returns a `QueryList` page object: + +| Property/Method | Description | +| ------------------------- | ----------------------------------------------- | +| `result.isLastPage` | Whether this is the last page | +| `result.getCurrentPage()` | Returns the current page as an array of entries | +| `result.nextPage()` | Move to the next page | + +Each entry is `{key, value, updateTime, createTime, version}`. + +```js +var result = store.list({ pageSize: 10, ascending: false }); + +// Iterate current page +var page = result.getCurrentPage(); +for (var i = 0; i < page.length; i++) { + console.log(page[i].key + ": " + page[i].value); +} + +// Next page +if (!result.isLastPage) { + result.nextPage(); +} +``` + +## Memory Cache & Persistence + +All `GameDataStorage` instances share a memory cache (`ConcurrentHashMap`). Data is loaded from disk on first access; subsequent reads/writes operate in memory. Every write operation (`set`/`update`/`remove`/`increment`) syncs to disk immediately. + +- **Same-name storage**: multiple `getDataStorage` calls for the same file path share a single in-memory copy, avoiding redundant I/O +- **Project isolation**: `getDataStorage("scores")` accesses different files in different projects (auto-prefixed with project name) +- **Cross-project sharing**: `getGroupStorage("leaderboard")` accesses the same `__shared__/leaderboard.json` from all projects + +## Complete Example: Leaderboard + +```js +// Cross-project shared leaderboard — all projects read/write the same data +var lb = storage.getGroupStorage("leaderboard"); + +// Save score +function saveScore(name, time) { + lb.set(name, time); +} + +saveScore("Steve", 12345); +saveScore("Alex", 9800); + +// Iterate all entries +var result = lb.list({ pageSize: 10, ascending: true }); +while (true) { + var page = result.getCurrentPage(); + for (var i = 0; i < page.length; i++) { + console.log(page[i].key + ": " + page[i].value); + } + if (result.isLastPage) break; + result.nextPage(); +} +``` + +All ✅ Box3 API. diff --git a/Box3JS-NeoForge-1.21.1/docs/api/voxels.md b/Box3JS-NeoForge-1.21.1/docs/api/voxels.md new file mode 100644 index 00000000..2aa59c6a --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/voxels.md @@ -0,0 +1,181 @@ +# voxels — 方块操作 API + +`voxels` 提供纯方块层面的读写操作。不涉及实体逻辑。 + +## 方块信息 + +### voxels.shape + +✅ Box3 API | 只读 `GameVector3`。世界尺寸(仅在部分 Box3 环境中有效)。 + +### voxels.VoxelTypes + +✅ Box3 API | 只读字符串数组。所有已注册方块名称列表。 + +## 名称 ↔ ID + +### voxels.id(name) + +✅ Box3 API | 方块名称 → 内部 ID。`name` 为带命名空间的字符串(如 `"minecraft:stone"`)。 + +### voxels.name(id) + +✅ Box3 API | 内部 ID → 方块名称。 + +```js +var stoneId = voxels.id("minecraft:stone"); // 获取 ID +var name = voxels.name(stoneId); // "minecraft:stone" +``` + +## 放置方块 + +### voxels.setVoxel(x, y, z, voxel) + +✅ Box3 API | 在指定坐标放置方块。`voxel` 参数接受: + +- 字符串:命名空间 ID,如 `"minecraft:glass"` +- 数字:内部方块 ID(含 rotation 编码) + +返回新放置方块的内部 ID。 + +### voxels.setVoxel(pos, voxel) + +⬆ GameVector3 重载。 + +### voxels.setVoxel(x, y, z, voxel, rotation) + +✅ Box3 API | 放置方块并指定旋转方向。`rotation` 为 0–3,控制朝向(类似 `BlockState` 的旋转)。 + +### voxels.setVoxel(pos, voxel, rotation) + +⬆ GameVector3 重载。 + +```js +// 用字符串放置 +voxels.setVoxel(0, 100, 0, "minecraft:glass"); +voxels.setVoxel(new GameVector3(0, 100, 0), "minecraft:gold_block"); + +// 指定旋转 +voxels.setVoxel(0, 100, 0, "minecraft:oak_stairs", 2); +voxels.setVoxel(new GameVector3(0, 100, 0), "minecraft:oak_stairs", 2); +``` + +### voxels.setVoxelId(x, y, z, voxelId) + +✅ Box3 API | 放置方块,`voxelId` 为已编码 rotation 的内部 ID。 + +### voxels.setVoxelId(pos, voxelId) + +⬆ GameVector3 重载。 + +### voxels.fillVoxel(x1, y1, z1, x2, y2, z2, voxel) + +⬆ MC 扩展 | 在矩形区域内填充方块。坐标两端点会被自动排序(无需保证 x1≤x2)。 + +### voxels.fillVoxel(pos1, pos2, voxel) + +⬆ GameVector3 重载。 + +```js +// 填充一个 5×1×5 的平台 +voxels.fillVoxel(-2, 100, -2, 2, 100, 2, "minecraft:white_concrete"); +voxels.fillVoxel( + new GameVector3(-2, 100, -2), + new GameVector3(2, 100, 2), + "minecraft:white_concrete", +); + +// 清除区域 +voxels.fillVoxel( + new GameVector3(-5, 100, -5), + new GameVector3(5, 110, 5), + "minecraft:air", +); +``` + +## 读取方块 + +### voxels.getVoxel(x, y, z) + +✅ Box3 API | 返回方块的基础 ID(不含 rotation 信息)。 + +### voxels.getVoxel(pos) + +⬆ GameVector3 重载。 + +### voxels.getVoxelId(x, y, z) + +✅ Box3 API | 返回完整 ID(含 rotation 编码位)。 + +### voxels.getVoxelId(pos) + +⬆ GameVector3 重载。 + +### voxels.getVoxelName(x, y, z) + +✅ Box3 API | 返回方块的命名空间 ID 字符串。 + +### voxels.getVoxelName(pos) + +⬆ GameVector3 重载。 + +### voxels.getVoxelRotation(x, y, z) + +✅ Box3 API | 返回方块的 rotation 值(0–3)。 + +### voxels.getVoxelRotation(pos) + +⬆ GameVector3 重载。 + +```js +var id = voxels.getVoxel(0, 100, 0); // 基础 ID +var fullId = voxels.getVoxelId(0, 100, 0); // 含 rotation 的完整 ID +var name = voxels.getVoxelName(0, 100, 0); // "minecraft:stone" +var rot = voxels.getVoxelRotation(0, 100, 0); // 0-3 + +// GameVector3 重载 +var id = voxels.getVoxel(entity.position); +var name = voxels.getVoxelName(new GameVector3(0, 100, 0)); +``` + +### voxels.countVoxel(x1, y1, z1, x2, y2, z2, voxel) + +⬆ MC 扩展 | 统计区域内匹配方块的个数。`voxel` 可以是字符串或数字 ID。 + +### voxels.countVoxel(pos1, pos2, voxel) + +⬆ GameVector3 重载。 + +```js +// 统计区域内有多少个钻石块 +var count = voxels.countVoxel( + -10, + 50, + -10, + 10, + 80, + 10, + "minecraft:diamond_block", +); +var count = voxels.countVoxel( + new GameVector3(-10, 50, -10), + new GameVector3(10, 80, 10), + "minecraft:diamond_block", +); +``` + +## 刷怪笼 + +### voxels.setSpawner(x, y, z, entityType) + +⬆ MC 扩展 | 设置坐标处刷怪笼的刷出类型。只有该坐标是 `minecraft:spawner` 时才有效。 + +### voxels.setSpawner(pos, entityType) + +⬆ GameVector3 重载。 + +```js +voxels.setVoxel(0, 100, 0, "minecraft:spawner"); +voxels.setSpawner(0, 100, 0, "minecraft:zombie"); +voxels.setSpawner(new GameVector3(0, 100, 0), "minecraft:skeleton"); +``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/voxels_en.md b/Box3JS-NeoForge-1.21.1/docs/api/voxels_en.md new file mode 100644 index 00000000..c6459497 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/voxels_en.md @@ -0,0 +1,181 @@ +# voxels — Block Operations API + +`voxels` provides pure block-level read/write operations. No entity logic is involved. + +## Block Info + +### voxels.shape + +✅ Box3 API | Read-only `GameVector3`. World dimensions (valid in some Box3 environments). + +### voxels.VoxelTypes + +✅ Box3 API | Read-only string array. All registered block names. + +## Name ↔ ID + +### voxels.id(name) + +✅ Box3 API | Block name → internal ID. `name` is a namespaced string (e.g. `"minecraft:stone"`). + +### voxels.name(id) + +✅ Box3 API | Internal ID → block name. + +```js +var stoneId = voxels.id("minecraft:stone"); // get ID +var name = voxels.name(stoneId); // "minecraft:stone" +``` + +## Placing Blocks + +### voxels.setVoxel(x, y, z, voxel) + +✅ Box3 API | Place a block at the given coordinates. `voxel` accepts: + +- String: namespaced ID, e.g. `"minecraft:glass"` +- Number: internal block ID (rotation encoded) + +Returns the internal ID of the newly placed block. + +### voxels.setVoxel(pos, voxel) + +⬆ GameVector3 overload. + +### voxels.setVoxel(x, y, z, voxel, rotation) + +✅ Box3 API | Place a block with rotation. `rotation` is 0–3, controlling orientation (like `BlockState` rotation). + +### voxels.setVoxel(pos, voxel, rotation) + +⬆ GameVector3 overload. + +```js +// Place by string +voxels.setVoxel(0, 100, 0, "minecraft:glass"); +voxels.setVoxel(new GameVector3(0, 100, 0), "minecraft:gold_block"); + +// With rotation +voxels.setVoxel(0, 100, 0, "minecraft:oak_stairs", 2); +voxels.setVoxel(new GameVector3(0, 100, 0), "minecraft:oak_stairs", 2); +``` + +### voxels.setVoxelId(x, y, z, voxelId) + +✅ Box3 API | Place a block, `voxelId` is the internal ID with encoded rotation. + +### voxels.setVoxelId(pos, voxelId) + +⬆ GameVector3 overload. + +### voxels.fillVoxel(x1, y1, z1, x2, y2, z2, voxel) + +⬆ MC Extension | Fill a rectangular region with a block. Corner coordinates are auto-sorted (no need to ensure x1 ≤ x2). + +### voxels.fillVoxel(pos1, pos2, voxel) + +⬆ GameVector3 overload. + +```js +// Fill a 5×1×5 platform +voxels.fillVoxel(-2, 100, -2, 2, 100, 2, "minecraft:white_concrete"); +voxels.fillVoxel( + new GameVector3(-2, 100, -2), + new GameVector3(2, 100, 2), + "minecraft:white_concrete", +); + +// Clear a region +voxels.fillVoxel( + new GameVector3(-5, 100, -5), + new GameVector3(5, 110, 5), + "minecraft:air", +); +``` + +## Reading Blocks + +### voxels.getVoxel(x, y, z) + +✅ Box3 API | Returns the block's base ID (without rotation info). + +### voxels.getVoxel(pos) + +⬆ GameVector3 overload. + +### voxels.getVoxelId(x, y, z) + +✅ Box3 API | Returns the full ID (with rotation bits encoded). + +### voxels.getVoxelId(pos) + +⬆ GameVector3 overload. + +### voxels.getVoxelName(x, y, z) + +✅ Box3 API | Returns the block's namespaced ID string. + +### voxels.getVoxelName(pos) + +⬆ GameVector3 overload. + +### voxels.getVoxelRotation(x, y, z) + +✅ Box3 API | Returns the block's rotation value (0–3). + +### voxels.getVoxelRotation(pos) + +⬆ GameVector3 overload. + +```js +var id = voxels.getVoxel(0, 100, 0); // base ID +var fullId = voxels.getVoxelId(0, 100, 0); // full ID with rotation +var name = voxels.getVoxelName(0, 100, 0); // "minecraft:stone" +var rot = voxels.getVoxelRotation(0, 100, 0); // 0-3 + +// GameVector3 overloads +var id = voxels.getVoxel(entity.position); +var name = voxels.getVoxelName(new GameVector3(0, 100, 0)); +``` + +### voxels.countVoxel(x1, y1, z1, x2, y2, z2, voxel) + +⬆ MC Extension | Count matching blocks in a region. `voxel` can be a string or numeric ID. + +### voxels.countVoxel(pos1, pos2, voxel) + +⬆ GameVector3 overload. + +```js +// Count how many diamond blocks are in the region +var count = voxels.countVoxel( + -10, + 50, + -10, + 10, + 80, + 10, + "minecraft:diamond_block", +); +var count = voxels.countVoxel( + new GameVector3(-10, 50, -10), + new GameVector3(10, 80, 10), + "minecraft:diamond_block", +); +``` + +## Spawner Control + +### voxels.setSpawner(x, y, z, entityType) + +⬆ MC Extension | Set the spawn type of the spawner at the given coordinates. Only effective if that block is `minecraft:spawner`. + +### voxels.setSpawner(pos, entityType) + +⬆ GameVector3 overload. + +```js +voxels.setVoxel(0, 100, 0, "minecraft:spawner"); +voxels.setSpawner(0, 100, 0, "minecraft:zombie"); +voxels.setSpawner(new GameVector3(0, 100, 0), "minecraft:skeleton"); +``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/world.md b/Box3JS-NeoForge-1.21.1/docs/api/world.md new file mode 100644 index 00000000..61e47fae --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/world.md @@ -0,0 +1,660 @@ +# world — 世界 API + +`world` 是全局单例,代表 Minecraft 服务端的世界状态。控制天气、时间、游戏规则、实体生成,注册事件回调,管理记分板/Bossbar/队伍,以及发射粒子、烟花、闪电等视觉效果。 + +## 世界属性 + +### world.projectName() + +⬆ MC 扩展 | 只读。服务端 MOTD 字符串。 + +```js +console.log(world.projectName()); // "A Minecraft Server" +``` + +### world.currentTick() + +✅ Box3 API | 只读。服务器自启动以来的总 tick 数。 + +```js +var uptime = world.currentTick(); +world.say("服务器已运行 " + Math.floor(uptime / 20 / 60) + " 分钟"); +``` + +## 天气 + +### world.rainDensity + +✅ Box3 API | 获取/设置降雨强度,范围 0.0–1.0。 + +```js +world.rainDensity = 1.0; // 满强度下雨 +console.log(world.rainDensity); // 0.0 ~ 1.0 +``` + +### world.thunderDensity + +⬆ MC 扩展 | 获取/设置雷暴强度,范围 0.0–1.0。 + +```js +world.thunderDensity = 0.5; +``` + +### world.clearWeather() + +⬆ MC 扩展 | 同时清除雨和雷暴。 + +```js +world.clearWeather(); +``` + +## 时间 + +### world.time + +✅ Box3 API | 获取/设置世界时间(tick)。Minecraft 一天 = 24000 tick。 + +```js +world.time = 6000; // 正午 +world.time = 18000; // 午夜 +console.log(world.time); // 当前时间 +``` + +此外提供 `world.setTime(tick)` 方法作为便捷设置接口。 + +```js +world.setTime(6000); // 等效于 world.time = 6000 +``` + +常用时间值:`0` 日出、`6000` 正午、`12000` 日落、`18000` 午夜。 + +### world.timeScale + +✅ Box3 API | 获取/设置时间流速。`0` = 暂停,`1` = 正常。底层修改 `doDaylightCycle` 游戏规则。 + +```js +world.timeScale = 0; // 冻结时间 +world.timeScale = 1; // 恢复正常 +``` + +## 难度 + +### world.difficulty + +✅ Box3 API | 获取/设置游戏难度。get 返回名称字符串,set 接受名称字符串或数字 0–3。 + +```js +world.difficulty = "hard"; +world.difficulty = 3; // 等同 hard +console.log(world.difficulty); // "hard" + +// 有效值: "peaceful"(0), "easy"(1), "normal"(2), "hard"(3) +``` + +## 出生点 + +### world.spawnPoint + +⬆ MC 扩展 | 只读,返回世界出生点 `GameVector3`。 + +### world.setWorldSpawn(pos) + +⬆ MC 扩展 | 设置世界出生点。 + +```js +world.setWorldSpawn(new GameVector3(0, 70, 0)); +``` + +## 游戏规则 + +### world.getGameRule(name) + +⬆ MC 扩展 | 获取游戏规则布尔值。 + +### world.setGameRule(name, value) + +⬆ MC 扩展 | 设置游戏规则。`value` 为布尔值。 + +**支持的规则:** + +| 规则名 | 说明 | +| -------------------- | ------------ | +| `doDaylightCycle` | 时间流动 | +| `doWeatherCycle` | 天气变化 | +| `keepInventory` | 死亡不掉落 | +| `doMobSpawning` | 生物自然生成 | +| `doFireTick` | 火焰蔓延 | +| `mobGriefing` | 生物破坏方块 | +| `doImmediateRespawn` | 立即重生 | + +```js +world.setGameRule("keepInventory", true); +world.setGameRule("doFireTick", false); +console.log(world.getGameRule("doMobSpawning")); // true/false +``` + +## 实体生成 + +### world.spawnEntity(type, pos) + +✅ Box3 API | 在指定位置生成实体。`type` 为命名空间 ID,返回 `Box3JSEntity`。 + +```js +var zombie = world.spawnEntity("minecraft:zombie", new GameVector3(0, 100, 0)); +zombie.setNameTag("守卫"); +zombie.maxHp = 40; +zombie.hp = 40; +zombie.setEquipment("mainhand", "minecraft:iron_sword"); +zombie.setAI(true); +``` + +## 事件回调 + +所有事件回调由 `world.onXxx(handler)` 注册。除 `onTick` 外,回调第一个参数通常是触发该事件的 `entity`(`Box3JSEntity`)。 + +| 事件 | 类型 | 回调签名 | 触发时机 | +| ---------------------------- | ------- | ------------------------------------------------------ | ------------------------------- | +| `world.onTick(fn)` | ✅ Box3 | `()` | 每 tick | +| `world.onPlayerJoin(fn)` | ✅ Box3 | `(entity)` | 玩家登录 | +| `world.onPlayerLeave(fn)` | ✅ Box3 | `(entity)` | 玩家退出 | +| `world.onChat(fn)` | ✅ Box3 | `(entity, message, tick)` | 玩家发送聊天消息 | +| `world.onVoxelDestroy(fn)` | ✅ Box3 | `(entity, x, y, z, voxel, tick)` | 玩家破坏方块 | +| `world.onBlockPlace(fn)` | ⬆ MC | `(entity, x, y, z, voxel, voxelId, tick)` | 玩家放置方块 | +| `world.onBlockActivate(fn)` | ⬆ MC | `(entity, x, y, z, voxel, tick)` | 玩家右键方块 | +| `world.onInteract(fn)` | ✅ Box3 | `(entity, target, tick)` | 玩家右键实体 | +| `world.onVoxelContact(fn)` | ✅ Box3 | `(entity, voxelId, x, y, z, contactType, force, tick)` | 实体接触方块 | +| `world.onEntityContact(fn)` | ✅ Box3 | `(entity, other, tick)` | 两个实体接触 | +| `world.onEntitySeparate(fn)` | ✅ Box3 | `(entity, other, tick)` | 两个实体分离 | +| `world.onFluidEnter(fn)` | ✅ Box3 | `(entity, fluid, x, y, z, tick)` | 实体进入液体 | +| `world.onFluidLeave(fn)` | ✅ Box3 | `(entity, fluid, x, y, z, tick)` | 实体离开液体 | +| `world.onEntityDeath(fn)` | ⬆ MC | `(entity, killer, tick)` | 实体死亡;`killer` 可能为 null | +| `world.onEntityDamage(fn)` | ⬆ MC | `(entity, amount, source, attacker, tick)` | 实体受伤(Pre 阶段) | +| `world.onPlayerRespawn(fn)` | ⬆ MC | `(entity)` | 玩家重生 | +| `world.onMessage(fn)` | ⬆ MC | `(from, data)` | 收到 `world.sendMessage()` 消息 | + +```js +world.onTick(() => { + // 每 tick 执行 +}); + +world.onPlayerJoin((entity) => { + var p = entity.player; + world.say(p.name + " 加入了游戏"); + p.teleport(new GameVector3(0, 100, 0)); +}); + +world.onChat((entity, message, tick) => { + var p = entity.player; + if (message === "!spawn") { + p.teleport(new GameVector3(0, 100, 0)); + } +}); + +world.onEntityDeath((entity, killer) => { + if (killer && killer.isPlayer()) { + var kp = killer.player; + kp.addExperienceLevels(1); + } +}); +``` + +## 查询 + +### world.querySelectorAll(selector) + +✅ Box3 API | 查询所有匹配实体。返回 `Box3JSEntity[]`。 + +### world.querySelector(selector) + +✅ Box3 API | 查询单个匹配实体。返回 `Box3JSEntity` 或 null。 + +**选择器语法:** + +| 选择器 | 含义 | +| ------------ | ---------------- | +| `"*"` | 所有在线玩家 | +| `"#uuid"` | 按 UUID 精确匹配 | +| `".tagName"` | 按标签匹配 | + +```js +var allPlayers = world.querySelectorAll("*"); +for (var i = 0; i < allPlayers.length; i++) { + var p = allPlayers[i].player; + p.actionBar("在线人数: " + allPlayers.length); +} + +var specific = world.querySelector("#550e8400-e29b-41d4-a716-446655440000"); +if (specific) { + specific.player.directMessage("找到你了"); +} +``` + +### world.say(message) + +✅ Box3 API | 向全服广播消息。 + +```js +world.say("§6[公告] §f比赛即将开始!"); +``` + +## 计时器 + +### world.setTimeout(handler, ticks) + +⬆ MC 扩展 | 延迟 `ticks` 后执行一次,返回 timer ID。 + +### world.setInterval(handler, ticks) + +⬆ MC 扩展 | 每 `ticks` 重复执行,返回 timer ID。 + +### world.clearTimeout(id) + +⬆ MC 扩展 | 取消 timeout。 + +### world.clearInterval(id) + +⬆ MC 扩展 | 取消 interval。 + +```js +var tid = world.setTimeout(() => { + world.say("3 秒后执行"); +}, 60); // 60 ticks = 3 秒 + +var iid = world.setInterval(() => { + world.say("每 10 秒执行一次"); +}, 200); // 200 ticks = 10 秒 + +// 取消 +world.clearTimeout(tid); +world.clearInterval(iid); +``` + +## 记分板 + +全部 ⬆ MC 扩展。 + +### world.addScoreboard(name) + +创建 dummy 类型记分项。 + +### world.addScoreboard(name, criteria) + +创建指定标准记分项。`criteria` 可选 `"dummy"`(手动修改)、`"deathCount"`(死亡计数)等。 + +### world.removeScoreboard(name) + +删除记分项。 + +### world.setScore(entityOrName, objectiveName, value) + +设置实体或名字的分数。`entityOrName` 可以是 `Box3JSEntity` 或字符串。 + +### world.getScore(entityOrName, objectiveName) + +获取分数。 + +### world.showScoreboard(slot, objectiveName) + +在指定位置显示记分板。`slot`:`"sidebar"`、`"list"`(Tab 列表)、`"belowname"`(名字下方)。 + +### world.hideScoreboard(slot) + +清除槽位。 + +### world.listScores(objectiveName) + +获取记分项所有条目,返回 `[{name, value}]`。 + +```js +world.addScoreboard("kills"); +world.setScore("Steve", "kills", 5); +world.showScoreboard("sidebar", "kills"); + +var scores = world.listScores("kills"); +// [{name: "Steve", value: 5}, ...] + +world.hideScoreboard("sidebar"); +world.removeScoreboard("kills"); +``` + +## Boss 血条 + +全部 ⬆ MC 扩展。 + +### world.showBossbar(name, text, progress, color) + +显示或更新 Boss 血条。 + +| 参数 | 说明 | +| ---------- | ------------------------------------------------------------------------- | +| `name` | 血条 ID,用于后续更新或移除 | +| `text` | 显示文本(支持颜色代码) | +| `progress` | 0.0–1.0,进度条长度 | +| `color` | `"blue"`、`"green"`、`"pink"`、`"purple"`、`"red"`、`"white"`、`"yellow"` | + +### world.removeBossbar(name) + +移除血条。 + +```js +// 创建一个 3 分钟倒计时血条 +var totalTicks = 3600; +var iid = world.setInterval(() => { + totalTicks -= 20; + var remain = totalTicks / 3600; + if (remain <= 0) { + world.removeBossbar("timer"); + world.clearInterval(iid); + } else { + world.showBossbar( + "timer", + "§e剩余时间: §f" + Math.ceil(totalTicks / 20) + "s", + remain, + remain > 0.5 ? "green" : remain > 0.2 ? "yellow" : "red", + ); + } +}, 20); +``` + +## 队伍 + +全部 ⬆ MC 扩展。 + +### world.createTeam(name, color) + +创建队伍。`color`:`"aqua"`、`"black"`、`"blue"`、`"dark_aqua"`、`"dark_blue"`、`"dark_gray"`、`"dark_green"`、`"dark_purple"`、`"dark_red"`、`"gold"`、`"gray"`、`"green"`、`"light_purple"`、`"red"`、`"white"`、`"yellow"`。 + +### world.removeTeam(name) + +删除队伍。 + +### world.joinTeam(entity, teamName) + +将实体加入队伍。 + +### world.leaveTeam(entity) + +将实体移出当前队伍。 + +### world.getTeamOf(entity) + +获取实体所在队伍名称。 + +```js +world.createTeam("red_team", "red"); +world.createTeam("blue_team", "blue"); + +world.onPlayerJoin((entity) => { + // 交替分边 + var online = world.querySelectorAll("*").length; + world.joinTeam(entity, online % 2 === 0 ? "red_team" : "blue_team"); +}); +``` + +## 世界边界 + +全部 ⬆ MC 扩展。 + +### world.borderSize + +获取/设置当前边界大小。 + +### world.setBorderCenter(x, z) + +设置边界中心。 + +### world.shrinkBorder(targetSize, seconds) + +边界平滑缩小到目标大小,耗时 `seconds` 秒。 + +### world.setBorderDamage(damagePerBlock) + +边界外每秒伤害值。 + +### world.setBorderWarning(blocks) + +边界警告距离(屏幕变红的提前量)。 + +```js +// 缩圈玩法 +world.setBorderCenter(0, 0); +world.borderSize = 500; +world.setBorderDamage(2); +world.setBorderWarning(10); + +world.setTimeout(() => { + world.shrinkBorder(100, 120); // 2 分钟缩到 100 +}, 600); // 30 秒后开始 +``` + +## 视觉效果 + +全部 ⬆ MC 扩展。 + +### world.strikeLightning(x, y, z) + +在坐标召唤闪电(造成默认伤害)。 + +### world.strikeLightning(pos) + +⬆ GameVector3 重载。 + +### world.strikeLightning(x, y, z, damage) + +召唤闪电并指定伤害值。 + +### world.strikeLightning(pos, damage) + +⬆ GameVector3 重载。 + +```js +world.strikeLightning(0, 100, 0); +world.strikeLightning(new GameVector3(0, 100, 0)); +world.strikeLightning(new GameVector3(0, 100, 0), 10); // 10 点伤害 +``` + +### world.launchFirework(x, y, z, color, shape) + +在坐标发射烟花火箭。 + +### world.launchFirework(pos, color, shape) + +⬆ GameVector3 重载。 + +**颜色:** `"red"`、`"blue"`、`"green"`、`"yellow"`、`"gold"`、`"white"`、`"aqua"`、`"pink"`、`"purple"` + +**形状:** `"ball"`(小球,默认)、`"large_ball"`(大球)、`"star"`(星形)、`"creeper"`(苦力怕脸)、`"burst"`(爆裂) + +```js +world.launchFirework(0, 100, 0, "gold", "large_ball"); +world.launchFirework(new GameVector3(0, 100, 0), "red", "star"); +``` + +### world.spawnParticle(type, x, y, z, count, dx, dy, dz, speed) + +在坐标生成粒子。粒子类型使用命名空间 ID。 + +### world.spawnParticle(type, pos, count, dx, dy, dz, speed) + +⬆ GameVector3 重载。 + +### world.spawnParticleCircle(x, y, z, radius, type, count) + +在水平圆形上均匀生成粒子。 + +### world.spawnParticleCircle(pos, radius, type, count) + +⬆ GameVector3 重载。 + +```js +// 单点粒子 +world.spawnParticle("minecraft:flame", 0, 100, 0, 10, 0.5, 0.5, 0.5, 0.1); +world.spawnParticle("minecraft:cloud", entity.position, 1, 0, 0, 0, 0); + +// 圆形粒子圈 +world.spawnParticleCircle(0, 100, 0, 2.0, "minecraft:happy_villager", 20); +world.spawnParticleCircle( + new GameVector3(0, 100, 0), + 2.0, + "minecraft:happy_villager", + 20, +); + +// 常用粒子: +// minecraft:flame, minecraft:cloud, minecraft:happy_villager +// minecraft:witch, minecraft:portal, minecraft:end_rod +// minecraft:heart, minecraft:note, minecraft:dragon_breath +``` + +## 物品 / 抛射物 + +全部 ⬆ MC 扩展。 + +### world.dropItem(x, y, z, itemId, count) + +在坐标掉落物品实体。 + +### world.dropItem(pos, itemId, count) + +⬆ GameVector3 重载。 + +```js +world.dropItem(0, 100, 0, "minecraft:diamond", 3); +world.dropItem(entity.position, "minecraft:diamond", 3); +``` + +### world.launchProjectile(type, x, y, z, tx, ty, tz, speed) + +从起点向目标发射抛射物,返回 `Box3JSEntity`。 + +### world.launchProjectile(type, pos, target, speed) + +⬆ GameVector3 重载,起点和目标均接受 `GameVector3`。 + +```js +// 从 (0, 100, 0) 向 (10, 100, 10) 发射火球 +world.launchProjectile("minecraft:fireball", 0, 100, 0, 10, 100, 10, 2); +world.launchProjectile( + "minecraft:fireball", + new GameVector3(0, 100, 0), + new GameVector3(10, 100, 10), + 2, +); + +// 发射箭 +world.launchProjectile("minecraft:arrow", 0, 100, 0, 5, 105, 0, 3); +``` + +## 爆炸 / 音效 / 查询 + +全部 ⬆ MC 扩展。 + +### world.explode(x, y, z, power) + +创建爆炸。 + +### world.explode(pos, power) + +⬆ GameVector3 重载。 + +### world.explode(x, y, z, power, fire) + +创建爆炸(`fire=true` 可引燃方块)。 + +### world.explode(pos, power, fire) + +⬆ GameVector3 重载。 + +```js +world.explode(0, 100, 0, 4); // 威力 4,不引火 +world.explode(new GameVector3(0, 100, 0), 8, true); // 威力 8,引火 +``` + +### world.playSound(path, x, y, z, volume, pitch) + +在坐标播放音效给所有在线玩家。`path` 为音效命名空间 ID,`volume` 0–1,`pitch` 0.5–2.0。 + +### world.playSound(path, pos, volume, pitch) + +⬆ GameVector3 重载。 + +```js +world.playSound("minecraft:block.note_block.pling", 0, 100, 0, 1.0, 1.5); +world.playSound( + "minecraft:block.note_block.pling", + new GameVector3(0, 100, 0), + 1.0, + 1.5, +); +``` + +### world.raycast(origin, direction) + +从 `origin` 向 `direction` 发射射线,默认最大距离 5 格。 + +### world.raycast(origin, direction, maxDistance) + +自定义最大距离的射线检测。 + +**返回值:** `{hit, x, y, z, normalX, normalY, normalZ, distance, entity, voxel}` + +```js +var dir = new GameVector3(0, -1, 0); +var result = world.raycast(playerEntity.position, dir, 50); +if (result.hit) { + console.log("命中方块:", result.voxel, "距离:", result.distance); + if (result.entity) { + console.log("命中实体:", result.entity.entityType); + } +} +``` + +### world.entitiesInArea(pos1, pos2) + +返回 AABB 包围盒内所有实体。 + +### world.entitiesInRadius(x, y, z, radius) + +⬆ MC 扩展 | 返回球体范围内所有实体。`entitiesInArea` 的便捷封装。 + +### world.entitiesInRadius(pos, radius) + +⬆ GameVector3 重载。 + +```js +// 查找 10 格半径内的所有实体 +var nearby = world.entitiesInRadius(0, 100, 0, 10); +var nearby = world.entitiesInRadius(entity.position, 10); +for (var i = 0; i < nearby.length; i++) { + console.log(nearby[i].entityType); +} +``` + +### world.getBiome(x, y, z) + +⬆ MC 扩展 | 返回生物群系的命名空间 ID 字符串。 + +### world.getBiome(pos) + +⬆ GameVector3 重载。 + +```js +var biome = world.getBiome(0, 70, 0); +console.log(biome); // "minecraft:plains" +var biome = world.getBiome(entity.position); +``` + +## 跨脚本消息 + +### world.sendMessage(target, data) + +⬆ MC 扩展 | 发送消息给其他脚本项目。`target` 为 `"*"`(广播)或项目名。接收方用 `world.onMessage()` 监听。 + +### world.runCommand(cmd) + +⬆ MC 扩展 | 以服务器控制台身份执行命令。 + +```js +world.runCommand("time set day"); +world.runCommand("weather clear"); +``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/world_en.md b/Box3JS-NeoForge-1.21.1/docs/api/world_en.md new file mode 100644 index 00000000..558110a4 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/world_en.md @@ -0,0 +1,662 @@ +# world — World API + +`world` is a global singleton representing the Minecraft server's world state. Controls weather, time, game rules, entity spawning; registers event callbacks; manages scoreboards, bossbars, teams; and fires particles, fireworks, lightning, and other visual effects. + +## World Properties + +### world.projectName() + +⬆ MC Extension | Read-only. The server MOTD string. + +```js +console.log(world.projectName()); // "A Minecraft Server" +``` + +### world.currentTick() + +✅ Box3 API | Read-only. Total ticks since server startup. + +```js +var uptime = world.currentTick(); +world.say( + "Server has been running for " + Math.floor(uptime / 20 / 60) + " minutes", +); +``` + +## Weather + +### world.rainDensity + +✅ Box3 API | Get/set rain intensity, range 0.0–1.0. + +```js +world.rainDensity = 1.0; // full rain +console.log(world.rainDensity); // 0.0 ~ 1.0 +``` + +### world.thunderDensity + +⬆ MC Extension | Get/set thunderstorm intensity, range 0.0–1.0. + +```js +world.thunderDensity = 0.5; +``` + +### world.clearWeather() + +⬆ MC Extension | Clear both rain and thunder. + +```js +world.clearWeather(); +``` + +## Time + +### world.time + +✅ Box3 API | Get/set world time (ticks). One Minecraft day = 24000 ticks. + +```js +world.time = 6000; // noon +world.time = 18000; // midnight +console.log(world.time); // current time +``` + +Also provides `world.setTime(tick)` as a convenience setter. + +```js +world.setTime(6000); // equivalent to world.time = 6000 +``` + +Common time values: `0` sunrise, `6000` noon, `12000` sunset, `18000` midnight. + +### world.timeScale + +✅ Box3 API | Get/set time flow rate. `0` = paused, `1` = normal. Internally modifies the `doDaylightCycle` game rule. + +```js +world.timeScale = 0; // freeze time +world.timeScale = 1; // resume normal +``` + +## Difficulty + +### world.difficulty + +✅ Box3 API | Get/set game difficulty. Get returns the name string; set accepts a name string or number 0–3. + +```js +world.difficulty = "hard"; +world.difficulty = 3; // same as hard +console.log(world.difficulty); // "hard" + +// Valid values: "peaceful"(0), "easy"(1), "normal"(2), "hard"(3) +``` + +## Spawn Point + +### world.spawnPoint + +⬆ MC Extension | Read-only, returns the world spawn point as `GameVector3`. + +### world.setWorldSpawn(pos) + +⬆ MC Extension | Set the world spawn point. + +```js +world.setWorldSpawn(new GameVector3(0, 70, 0)); +``` + +## Game Rules + +### world.getGameRule(name) + +⬆ MC Extension | Get a game rule boolean value. + +### world.setGameRule(name, value) + +⬆ MC Extension | Set a game rule. `value` is a boolean. + +**Supported rules:** + +| Rule Name | Description | +| -------------------- | ----------------------- | +| `doDaylightCycle` | Time progression | +| `doWeatherCycle` | Weather changes | +| `keepInventory` | Keep inventory on death | +| `doMobSpawning` | Natural mob spawning | +| `doFireTick` | Fire spread | +| `mobGriefing` | Mob block griefing | +| `doImmediateRespawn` | Instant respawn | + +```js +world.setGameRule("keepInventory", true); +world.setGameRule("doFireTick", false); +console.log(world.getGameRule("doMobSpawning")); // true/false +``` + +## Entity Spawning + +### world.spawnEntity(type, pos) + +✅ Box3 API | Spawn an entity at the given position. `type` is a namespaced ID, returns `Box3JSEntity`. + +```js +var zombie = world.spawnEntity("minecraft:zombie", new GameVector3(0, 100, 0)); +zombie.setNameTag("Guard"); +zombie.maxHp = 40; +zombie.hp = 40; +zombie.setEquipment("mainhand", "minecraft:iron_sword"); +zombie.setAI(true); +``` + +## Event Callbacks + +All event callbacks are registered via `world.onXxx(handler)`. Except for `onTick`, the first callback parameter is usually the triggering `entity` (`Box3JSEntity`). + +| Event | Type | Callback Signature | Trigger | +| ---------------------------- | ------- | ------------------------------------------------------ | -------------------------------------- | +| `world.onTick(fn)` | ✅ Box3 | `()` | Every tick | +| `world.onPlayerJoin(fn)` | ✅ Box3 | `(entity)` | Player logs in | +| `world.onPlayerLeave(fn)` | ✅ Box3 | `(entity)` | Player leaves | +| `world.onChat(fn)` | ✅ Box3 | `(entity, message, tick)` | Player sends chat message | +| `world.onVoxelDestroy(fn)` | ✅ Box3 | `(entity, x, y, z, voxel, tick)` | Player breaks a block | +| `world.onBlockPlace(fn)` | ⬆ MC | `(entity, x, y, z, voxel, voxelId, tick)` | Player places a block | +| `world.onBlockActivate(fn)` | ⬆ MC | `(entity, x, y, z, voxel, tick)` | Player right-clicks a block | +| `world.onInteract(fn)` | ✅ Box3 | `(entity, target, tick)` | Player right-clicks an entity | +| `world.onVoxelContact(fn)` | ✅ Box3 | `(entity, voxelId, x, y, z, contactType, force, tick)` | Entity contacts a block | +| `world.onEntityContact(fn)` | ✅ Box3 | `(entity, other, tick)` | Two entities contact | +| `world.onEntitySeparate(fn)` | ✅ Box3 | `(entity, other, tick)` | Two entities separate | +| `world.onFluidEnter(fn)` | ✅ Box3 | `(entity, fluid, x, y, z, tick)` | Entity enters a fluid | +| `world.onFluidLeave(fn)` | ✅ Box3 | `(entity, fluid, x, y, z, tick)` | Entity leaves a fluid | +| `world.onEntityDeath(fn)` | ⬆ MC | `(entity, killer, tick)` | Entity dies; `killer` may be null | +| `world.onEntityDamage(fn)` | ⬆ MC | `(entity, amount, source, attacker, tick)` | Entity takes damage (Pre phase) | +| `world.onPlayerRespawn(fn)` | ⬆ MC | `(entity)` | Player respawns | +| `world.onMessage(fn)` | ⬆ MC | `(from, data)` | Receives `world.sendMessage()` message | + +```js +world.onTick(() => { + // runs every tick +}); + +world.onPlayerJoin((entity) => { + var p = entity.player; + world.say(p.name + " joined the game"); + p.teleport(new GameVector3(0, 100, 0)); +}); + +world.onChat((entity, message, tick) => { + var p = entity.player; + if (message === "!spawn") { + p.teleport(new GameVector3(0, 100, 0)); + } +}); + +world.onEntityDeath((entity, killer) => { + if (killer && killer.isPlayer()) { + var kp = killer.player; + kp.addExperienceLevels(1); + } +}); +``` + +## Query + +### world.querySelectorAll(selector) + +✅ Box3 API | Query all matching entities. Returns `Box3JSEntity[]`. + +### world.querySelector(selector) + +✅ Box3 API | Query a single matching entity. Returns `Box3JSEntity` or null. + +**Selector syntax:** + +| Selector | Meaning | +| ------------ | ------------------- | +| `"*"` | All online players | +| `"#uuid"` | Exact match by UUID | +| `".tagName"` | Match by tag | + +```js +var allPlayers = world.querySelectorAll("*"); +for (var i = 0; i < allPlayers.length; i++) { + var p = allPlayers[i].player; + p.actionBar("Online: " + allPlayers.length); +} + +var specific = world.querySelector("#550e8400-e29b-41d4-a716-446655440000"); +if (specific) { + specific.player.directMessage("Found you"); +} +``` + +### world.say(message) + +✅ Box3 API | Broadcast a message to the entire server. + +```js +world.say("§6[Announcement] §fThe match is about to begin!"); +``` + +## Timers + +### world.setTimeout(handler, ticks) + +⬆ MC Extension | Execute once after `ticks` delay, returns timer ID. + +### world.setInterval(handler, ticks) + +⬆ MC Extension | Execute repeatedly every `ticks`, returns timer ID. + +### world.clearTimeout(id) + +⬆ MC Extension | Cancel a timeout. + +### world.clearInterval(id) + +⬆ MC Extension | Cancel an interval. + +```js +var tid = world.setTimeout(() => { + world.say("Executed after 3 seconds"); +}, 60); // 60 ticks = 3 seconds + +var iid = world.setInterval(() => { + world.say("Executed every 10 seconds"); +}, 200); // 200 ticks = 10 seconds + +// Cancel +world.clearTimeout(tid); +world.clearInterval(iid); +``` + +## Scoreboard + +All ⬆ MC Extension. + +### world.addScoreboard(name) + +Create a dummy-type objective. + +### world.addScoreboard(name, criteria) + +Create an objective with a specific criteria. `criteria` can be `"dummy"` (manual), `"deathCount"`, etc. + +### world.removeScoreboard(name) + +Delete an objective. + +### world.setScore(entityOrName, objectiveName, value) + +Set the score for an entity or name. `entityOrName` can be a `Box3JSEntity` or a string. + +### world.getScore(entityOrName, objectiveName) + +Get a score. + +### world.showScoreboard(slot, objectiveName) + +Display a scoreboard at the given slot. `slot`: `"sidebar"`, `"list"` (tab list), `"belowname"`. + +### world.hideScoreboard(slot) + +Clear a slot. + +### world.listScores(objectiveName) + +Get all entries for an objective, returns `[{name, value}]`. + +```js +world.addScoreboard("kills"); +world.setScore("Steve", "kills", 5); +world.showScoreboard("sidebar", "kills"); + +var scores = world.listScores("kills"); +// [{name: "Steve", value: 5}, ...] + +world.hideScoreboard("sidebar"); +world.removeScoreboard("kills"); +``` + +## Boss Bar + +All ⬆ MC Extension. + +### world.showBossbar(name, text, progress, color) + +Show or update a boss bar. + +| Parameter | Description | +| ---------- | ------------------------------------------------------------------------- | +| `name` | Bar ID, used for subsequent updates or removal | +| `text` | Display text (supports color codes) | +| `progress` | 0.0–1.0, bar fill length | +| `color` | `"blue"`, `"green"`, `"pink"`, `"purple"`, `"red"`, `"white"`, `"yellow"` | + +### world.removeBossbar(name) + +Remove a boss bar. + +```js +// Create a 3-minute countdown boss bar +var totalTicks = 3600; +var iid = world.setInterval(() => { + totalTicks -= 20; + var remain = totalTicks / 3600; + if (remain <= 0) { + world.removeBossbar("timer"); + world.clearInterval(iid); + } else { + world.showBossbar( + "timer", + "§eTime remaining: §f" + Math.ceil(totalTicks / 20) + "s", + remain, + remain > 0.5 ? "green" : remain > 0.2 ? "yellow" : "red", + ); + } +}, 20); +``` + +## Teams + +All ⬆ MC Extension. + +### world.createTeam(name, color) + +Create a team. `color`: `"aqua"`, `"black"`, `"blue"`, `"dark_aqua"`, `"dark_blue"`, `"dark_gray"`, `"dark_green"`, `"dark_purple"`, `"dark_red"`, `"gold"`, `"gray"`, `"green"`, `"light_purple"`, `"red"`, `"white"`, `"yellow"`. + +### world.removeTeam(name) + +Delete a team. + +### world.joinTeam(entity, teamName) + +Add an entity to a team. + +### world.leaveTeam(entity) + +Remove an entity from its current team. + +### world.getTeamOf(entity) + +Get the team name an entity belongs to. + +```js +world.createTeam("red_team", "red"); +world.createTeam("blue_team", "blue"); + +world.onPlayerJoin((entity) => { + // Alternate team assignment + var online = world.querySelectorAll("*").length; + world.joinTeam(entity, online % 2 === 0 ? "red_team" : "blue_team"); +}); +``` + +## World Border + +All ⬆ MC Extension. + +### world.borderSize + +Get/set the current border size. + +### world.setBorderCenter(x, z) + +Set the border center. + +### world.shrinkBorder(targetSize, seconds) + +Smoothly shrink the border to the target size over `seconds` seconds. + +### world.setBorderDamage(damagePerBlock) + +Damage per second outside the border. + +### world.setBorderWarning(blocks) + +Border warning distance (screen reddening advance notice). + +```js +// Shrinking zone gameplay +world.setBorderCenter(0, 0); +world.borderSize = 500; +world.setBorderDamage(2); +world.setBorderWarning(10); + +world.setTimeout(() => { + world.shrinkBorder(100, 120); // shrink to 100 over 2 minutes +}, 600); // start after 30 seconds +``` + +## Visual Effects + +All ⬆ MC Extension. + +### world.strikeLightning(x, y, z) + +Summon lightning at coordinates (default damage). + +### world.strikeLightning(pos) + +⬆ GameVector3 overload. + +### world.strikeLightning(x, y, z, damage) + +Summon lightning with custom damage. + +### world.strikeLightning(pos, damage) + +⬆ GameVector3 overload. + +```js +world.strikeLightning(0, 100, 0); +world.strikeLightning(new GameVector3(0, 100, 0)); +world.strikeLightning(new GameVector3(0, 100, 0), 10); // 10 damage +``` + +### world.launchFirework(x, y, z, color, shape) + +Launch a firework rocket at coordinates. + +### world.launchFirework(pos, color, shape) + +⬆ GameVector3 overload. + +**Colors:** `"red"`, `"blue"`, `"green"`, `"yellow"`, `"gold"`, `"white"`, `"aqua"`, `"pink"`, `"purple"` + +**Shapes:** `"ball"` (small ball, default), `"large_ball"`, `"star"`, `"creeper"`, `"burst"` + +```js +world.launchFirework(0, 100, 0, "gold", "large_ball"); +world.launchFirework(new GameVector3(0, 100, 0), "red", "star"); +``` + +### world.spawnParticle(type, x, y, z, count, dx, dy, dz, speed) + +Spawn particles at coordinates. Particle type uses namespaced ID. + +### world.spawnParticle(type, pos, count, dx, dy, dz, speed) + +⬆ GameVector3 overload. + +### world.spawnParticleCircle(x, y, z, radius, type, count) + +Spawn particles evenly on a horizontal circle. + +### world.spawnParticleCircle(pos, radius, type, count) + +⬆ GameVector3 overload. + +```js +// Point particles +world.spawnParticle("minecraft:flame", 0, 100, 0, 10, 0.5, 0.5, 0.5, 0.1); +world.spawnParticle("minecraft:cloud", entity.position, 1, 0, 0, 0, 0); + +// Circular particle ring +world.spawnParticleCircle(0, 100, 0, 2.0, "minecraft:happy_villager", 20); +world.spawnParticleCircle( + new GameVector3(0, 100, 0), + 2.0, + "minecraft:happy_villager", + 20, +); + +// Common particles: +// minecraft:flame, minecraft:cloud, minecraft:happy_villager +// minecraft:witch, minecraft:portal, minecraft:end_rod +// minecraft:heart, minecraft:note, minecraft:dragon_breath +``` + +## Items / Projectiles + +All ⬆ MC Extension. + +### world.dropItem(x, y, z, itemId, count) + +Drop an item entity at coordinates. + +### world.dropItem(pos, itemId, count) + +⬆ GameVector3 overload. + +```js +world.dropItem(0, 100, 0, "minecraft:diamond", 3); +world.dropItem(entity.position, "minecraft:diamond", 3); +``` + +### world.launchProjectile(type, x, y, z, tx, ty, tz, speed) + +Launch a projectile from start to target, returns `Box3JSEntity`. + +### world.launchProjectile(type, pos, target, speed) + +⬆ GameVector3 overload — both start and target accept `GameVector3`. + +```js +// Launch a fireball from (0, 100, 0) toward (10, 100, 10) +world.launchProjectile("minecraft:fireball", 0, 100, 0, 10, 100, 10, 2); +world.launchProjectile( + "minecraft:fireball", + new GameVector3(0, 100, 0), + new GameVector3(10, 100, 10), + 2, +); + +// Launch an arrow +world.launchProjectile("minecraft:arrow", 0, 100, 0, 5, 105, 0, 3); +``` + +## Explosion / Sound / Query + +All ⬆ MC Extension. + +### world.explode(x, y, z, power) + +Create an explosion. + +### world.explode(pos, power) + +⬆ GameVector3 overload. + +### world.explode(x, y, z, power, fire) + +Create an explosion (`fire=true` can ignite blocks). + +### world.explode(pos, power, fire) + +⬆ GameVector3 overload. + +```js +world.explode(0, 100, 0, 4); // power 4, no fire +world.explode(new GameVector3(0, 100, 0), 8, true); // power 8, with fire +``` + +### world.playSound(path, x, y, z, volume, pitch) + +Play a sound at coordinates to all online players. `path` is a sound namespaced ID, `volume` 0–1, `pitch` 0.5–2.0. + +### world.playSound(path, pos, volume, pitch) + +⬆ GameVector3 overload. + +```js +world.playSound("minecraft:block.note_block.pling", 0, 100, 0, 1.0, 1.5); +world.playSound( + "minecraft:block.note_block.pling", + new GameVector3(0, 100, 0), + 1.0, + 1.5, +); +``` + +### world.raycast(origin, direction) + +Cast a ray from `origin` in `direction`, default max distance 5 blocks. + +### world.raycast(origin, direction, maxDistance) + +Raycast with custom max distance. + +**Returns:** `{hit, x, y, z, normalX, normalY, normalZ, distance, entity, voxel}` + +```js +var dir = new GameVector3(0, -1, 0); +var result = world.raycast(playerEntity.position, dir, 50); +if (result.hit) { + console.log("Hit block:", result.voxel, "distance:", result.distance); + if (result.entity) { + console.log("Hit entity:", result.entity.entityType); + } +} +``` + +### world.entitiesInArea(pos1, pos2) + +Returns all entities within the AABB defined by two corner positions. + +### world.entitiesInRadius(x, y, z, radius) + +⬆ MC Extension | Returns all entities within a spherical radius. Convenience wrapper around `entitiesInArea`. + +### world.entitiesInRadius(pos, radius) + +⬆ GameVector3 overload. + +```js +// Find all entities within 10-block radius +var nearby = world.entitiesInRadius(0, 100, 0, 10); +var nearby = world.entitiesInRadius(entity.position, 10); +for (var i = 0; i < nearby.length; i++) { + console.log(nearby[i].entityType); +} +``` + +### world.getBiome(x, y, z) + +⬆ MC Extension | Returns the biome namespaced ID string. + +### world.getBiome(pos) + +⬆ GameVector3 overload. + +```js +var biome = world.getBiome(0, 70, 0); +console.log(biome); // "minecraft:plains" +var biome = world.getBiome(entity.position); +``` + +## Cross-script Messaging + +### world.sendMessage(target, data) + +⬆ MC Extension | Send a message to another script project. `target` is `"*"` (broadcast) or a project name. Receivers listen via `world.onMessage()`. + +### world.runCommand(cmd) + +⬆ MC Extension | Execute a command as the server console. + +```js +world.runCommand("time set day"); +world.runCommand("weather clear"); +``` diff --git a/Box3JS-NeoForge-1.21.1/gradle.properties b/Box3JS-NeoForge-1.21.1/gradle.properties new file mode 100644 index 00000000..ebbdd051 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/gradle.properties @@ -0,0 +1,39 @@ +# Sets default memory used for gradle commands. Can be overridden by user or command line properties. +org.gradle.jvmargs=-Xmx1G +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=true + +#read more on this at https://github.com/neoforged/ModDevGradle?tab=readme-ov-file#better-minecraft-parameter-names--javadoc-parchment +# you can also find the latest versions at: https://parchmentmc.org/docs/getting-started +parchment_minecraft_version=1.21.1 +parchment_mappings_version=2024.11.17 +# Environment Properties +# You can find the latest versions here: https://projects.neoforged.net/neoforged/neoforge +# The Minecraft version must agree with the Neo version to get a valid artifact +minecraft_version=1.21.1 +# The Minecraft version range can use any release version of Minecraft as bounds. +# Snapshots, pre-releases, and release candidates are not guaranteed to sort properly +# as they do not follow standard versioning conventions. +minecraft_version_range=[1.21,1.21.1] +# The Neo version must agree with the Minecraft version to get a valid artifact +neo_version=21.1.220 +# The loader version range can only use the major version of FML as bounds +loader_version_range=[1,) + +## Mod Properties + +# The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63} +# Must match the String constant located in the main mod class annotated with @Mod. +mod_id=box3js +# The human-readable display name for the mod. +mod_name=Box3JS +# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. +mod_license=Apache License 2.0 +# The mod version. See https://semver.org/ +mod_version=0.1.0-neoforge-mc1.21.1-beta +# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. +# This should match the base package used for the mod sources. +# See https://maven.apache.org/guides/mini/guide-naming-conventions.html +mod_group_id=com.box3lab.box3js diff --git a/Box3JS-NeoForge-1.21.1/gradle/wrapper/gradle-wrapper.jar b/Box3JS-NeoForge-1.21.1/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..1b33c55b Binary files /dev/null and b/Box3JS-NeoForge-1.21.1/gradle/wrapper/gradle-wrapper.jar differ diff --git a/Box3JS-NeoForge-1.21.1/gradle/wrapper/gradle-wrapper.properties b/Box3JS-NeoForge-1.21.1/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..23449a2b --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/Box3JS-NeoForge-1.21.1/gradlew b/Box3JS-NeoForge-1.21.1/gradlew new file mode 100755 index 00000000..23d15a93 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/Box3JS-NeoForge-1.21.1/gradlew.bat b/Box3JS-NeoForge-1.21.1/gradlew.bat new file mode 100644 index 00000000..db3a6ac2 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/Box3JS-NeoForge-1.21.1/settings.gradle b/Box3JS-NeoForge-1.21.1/settings.gradle new file mode 100644 index 00000000..7a488e36 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/settings.gradle @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + gradlePluginPortal() + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java new file mode 100644 index 00000000..1bc43438 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java @@ -0,0 +1,106 @@ +package com.box3lab.box3js; + +import com.box3lab.box3js.script.Box3ScriptCommand; +import com.box3lab.box3js.script.Box3ScriptEngine; +import com.mojang.logging.LogUtils; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.ServerChatEvent; +import net.neoforged.neoforge.event.entity.living.LivingDamageEvent; +import net.neoforged.neoforge.event.entity.living.LivingDeathEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent; +import net.neoforged.neoforge.event.level.BlockEvent; +import net.neoforged.neoforge.event.server.ServerStartedEvent; +import net.neoforged.neoforge.event.tick.ServerTickEvent; +import org.slf4j.Logger; + +@Mod(Box3JS.MODID) +public class Box3JS { + + public static final String MODID = "box3js"; + public static final Logger LOGGER = LogUtils.getLogger(); + + public Box3JS(IEventBus modEventBus, ModContainer modContainer) { + // Script commands + NeoForge.EVENT_BUS.addListener(Box3ScriptCommand::register); + + // Tick + NeoForge.EVENT_BUS.addListener((ServerTickEvent.Post event) -> { + Box3ScriptEngine.get().fireTick(); + }); + + // Player join / leave + NeoForge.EVENT_BUS.addListener((PlayerEvent.PlayerLoggedInEvent event) -> { + Box3ScriptEngine.get().firePlayerJoin((ServerPlayer) event.getEntity()); + }); + NeoForge.EVENT_BUS.addListener((PlayerEvent.PlayerLoggedOutEvent event) -> { + Box3ScriptEngine.get().firePlayerLeave((ServerPlayer) event.getEntity()); + }); + + // Block break + NeoForge.EVENT_BUS.addListener((BlockEvent.BreakEvent event) -> { + if (event.getPlayer() instanceof ServerPlayer sp) { + Box3ScriptEngine.get().fireVoxelDestroy(sp, event.getPos()); + } + }); + + // Block place + NeoForge.EVENT_BUS.addListener((BlockEvent.EntityPlaceEvent event) -> { + if (event.getEntity() instanceof ServerPlayer sp) { + Box3ScriptEngine.get().fireBlockPlace(sp, event.getPos(), event.getPlacedBlock()); + } + }); + + // Interact (entity) + NeoForge.EVENT_BUS.addListener((PlayerInteractEvent.EntityInteract event) -> { + if (event.getEntity() instanceof ServerPlayer sp) { + Box3ScriptEngine.get().fireInteract(sp, event.getTarget()); + } + }); + + // Right-click block + NeoForge.EVENT_BUS.addListener((PlayerInteractEvent.RightClickBlock event) -> { + if (event.getEntity() instanceof ServerPlayer sp) { + Box3ScriptEngine.get().fireBlockActivate(sp, event.getPos(), event.getLevel().getBlockState(event.getPos())); + } + }); + + // Chat + NeoForge.EVENT_BUS.addListener((ServerChatEvent event) -> { + if (event.getPlayer() instanceof ServerPlayer sp) { + Box3ScriptEngine.get().fireChat(sp, event.getMessage().getString()); + } + }); + + // Entity death + NeoForge.EVENT_BUS.addListener((LivingDeathEvent event) -> { + Box3ScriptEngine.get().fireEntityDeath(event.getEntity(), event.getSource().getEntity()); + }); + + // Player respawn + NeoForge.EVENT_BUS.addListener((PlayerEvent.PlayerRespawnEvent event) -> { + if (event.getEntity() instanceof ServerPlayer sp) { + Box3ScriptEngine.get().firePlayerRespawn(sp); + } + }); + + // Entity damage + NeoForge.EVENT_BUS.addListener((LivingDamageEvent.Pre event) -> { + Box3ScriptEngine.get().fireEntityDamage(event.getEntity(), + event.getNewDamage(), + event.getSource().getMsgId(), + event.getSource().getEntity()); + }); + + // Auto-load scripts from config/box3/script//app.js on server start + NeoForge.EVENT_BUS.addListener((ServerStartedEvent event) -> { + Box3ScriptEngine.get().autoLoad(event.getServer()); + }); + + LOGGER.info("Box3JS script engine initialized."); + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSBossbar.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSBossbar.java new file mode 100644 index 00000000..deb95e9d --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSBossbar.java @@ -0,0 +1,68 @@ +package com.box3lab.box3js.script; + +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.BossEvent.BossBarColor; +import net.minecraft.world.BossEvent.BossBarOverlay; +import net.minecraft.server.level.ServerBossEvent; + +import java.util.*; + +class Box3JSBossbar { + + private final MinecraftServer server; + private final Map> projectBossBars = new HashMap<>(); + + Box3JSBossbar(MinecraftServer server) { + this.server = server; + } + + void showBossbar(String project, String name, String text, double progress, String colorName) { + Map bars = projectBossBars.computeIfAbsent(project, k -> new HashMap<>()); + ServerBossEvent bar = bars.get(name); + if (bar == null) { + bar = new ServerBossEvent(Component.literal(text), resolveColor(colorName), BossBarOverlay.PROGRESS); + bars.put(name, bar); + } else { + bar.setName(Component.literal(text)); + if (colorName != null) bar.setColor(resolveColor(colorName)); + } + bar.setProgress((float) Math.max(0, Math.min(1, progress))); + for (ServerPlayer sp : server.getPlayerList().getPlayers()) bar.addPlayer(sp); + } + + void removeBossbar(String project, String name) { + Map bars = projectBossBars.get(project); + if (bars == null) return; + ServerBossEvent bar = bars.remove(name); + if (bar != null) bar.removeAllPlayers(); + } + + void removeProject(String project) { + Map bars = projectBossBars.remove(project); + if (bars != null) { + for (ServerBossEvent bar : bars.values()) bar.removeAllPlayers(); + } + } + + void resetAll() { + for (Map bars : projectBossBars.values()) { + for (ServerBossEvent bar : bars.values()) bar.removeAllPlayers(); + } + projectBossBars.clear(); + } + + private static BossBarColor resolveColor(String colorName) { + if (colorName == null) return BossBarColor.WHITE; + return switch (colorName.toLowerCase(Locale.ROOT)) { + case "red" -> BossBarColor.RED; + case "blue" -> BossBarColor.BLUE; + case "green" -> BossBarColor.GREEN; + case "yellow" -> BossBarColor.YELLOW; + case "purple" -> BossBarColor.PURPLE; + case "pink" -> BossBarColor.PINK; + default -> BossBarColor.WHITE; + }; + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSCallbacks.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSCallbacks.java new file mode 100644 index 00000000..bcb45646 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSCallbacks.java @@ -0,0 +1,81 @@ +package com.box3lab.box3js.script; + +@FunctionalInterface +interface PlayerJoinCallback { + void onJoin(Box3JSEntity entity); +} + +@FunctionalInterface +interface PlayerLeaveCallback { + void onLeave(Box3JSEntity entity); +} + +@FunctionalInterface +interface VoxelDestroyCallback { + void onDestroy(Box3JSEntity entity, int x, int y, int z, String voxel, long tick); +} + +@FunctionalInterface +interface VoxelContactCallback { + void onContact(Box3JSEntity entity, int voxel, int x, int y, int z, int axis, double force, long tick); +} + +@FunctionalInterface +interface InteractCallback { + void onInteract(Box3JSEntity entity, Box3JSEntity target, long tick); +} + +@FunctionalInterface +interface ChatCallback { + void onChat(Box3JSEntity entity, String message, long tick); +} + +@FunctionalInterface +interface FluidEnterCallback { + void onEnter(Box3JSEntity entity, String fluid, int x, int y, int z, long tick); +} + +@FunctionalInterface +interface FluidLeaveCallback { + void onLeave(Box3JSEntity entity, String fluid, int x, int y, int z, long tick); +} + +@FunctionalInterface +interface EntityContactCallback { + void onContact(Box3JSEntity entity, Box3JSEntity other, long tick); +} + +@FunctionalInterface +interface EntitySeparateCallback { + void onSeparate(Box3JSEntity entity, Box3JSEntity other, long tick); +} + +@FunctionalInterface +interface BlockPlaceCallback { + void onPlace(Box3JSEntity entity, int x, int y, int z, String voxel, int voxelId, long tick); +} + +@FunctionalInterface +interface EntityDeathCallback { + void onDeath(Box3JSEntity entity, Box3JSEntity killer, long tick); +} + +@FunctionalInterface +interface PlayerRespawnCallback { + void onRespawn(Box3JSEntity entity); +} + +@FunctionalInterface +interface BlockActivateCallback { + void onActivate(Box3JSEntity entity, int x, int y, int z, String voxel, long tick); +} + +@FunctionalInterface +interface EntityDamageCallback { + void onDamage(Box3JSEntity entity, double amount, String source, Box3JSEntity attacker, long tick); +} + +@FunctionalInterface +interface MessageCallback { + void onMessage(String from, Object data); +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEntity.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEntity.java new file mode 100644 index 00000000..d26d1575 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEntity.java @@ -0,0 +1,385 @@ +package com.box3lab.box3js.script; + +import net.minecraft.core.Holder; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.PathfinderMob; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import org.mozilla.javascript.Function; + +import java.util.Map; +import java.util.function.Consumer; + +public class Box3JSEntity { + + private final Entity entity; + private final MinecraftServer server; + private final Box3ScriptEngine engine; + private Box3JSPlayer playerProxy; + private Function _onDestroyHandler; + private final GameVector3 _position, _velocity, _bounds; + + public Box3JSEntity(Entity entity, MinecraftServer server, Box3ScriptEngine engine) { + this.entity = entity; + this.server = server; + this.engine = engine; + this._position = new LiveVec3(v -> entity.teleportTo(v.x, v.y, v.z)); + this._velocity = new LiveVec3(v -> entity.setDeltaMovement(v.x, v.y, v.z)); + this._bounds = new GameVector3(); + } + + public Entity getEntity() { return entity; } + + // ---- Identity ---- + + public String getId() { + return entity.getStringUUID(); + } + + public boolean isPlayer() { return entity instanceof ServerPlayer; } + + public String getEntityType() { + var key = entity.getType().builtInRegistryHolder().key(); + return key != null ? key.location().toString() : "unknown"; + } + + public Box3JSPlayer getPlayer() { + if (!isPlayer()) return null; + if (playerProxy == null) playerProxy = new Box3JSPlayer((ServerPlayer) entity, server, engine); + return playerProxy; + } + + // ---- Position / Velocity / Bounds ---- + + public GameVector3 getPosition() { + _position.x = entity.getX(); + _position.y = entity.getY(); + _position.z = entity.getZ(); + return _position; + } + + public GameVector3 getVelocity() { + var v = entity.getDeltaMovement(); + _velocity.x = v.x; _velocity.y = v.y; _velocity.z = v.z; + return _velocity; + } + + public GameVector3 getBounds() { + var bb = entity.getBoundingBox(); + _bounds.x = (bb.maxX - bb.minX) / 2.0; + _bounds.y = (bb.maxY - bb.minY) / 2.0; + _bounds.z = (bb.maxZ - bb.minZ) / 2.0; + return _bounds; + } + + // ---- Appearance ---- + + public boolean getMeshInvisible() { return getProp("meshInvisible", false); } + public void setMeshInvisible(boolean v) { + trackIfSandboxed(); + setProp("meshInvisible", v); + entity.setInvisible(v); + } + + // ---- Tags ---- + + public void addTag(String tag) { + trackIfSandboxed(); + entity.addTag(tag); + } + + public boolean hasTag(String tag) { + return entity.getTags().contains(tag); + } + + public void removeTag(String tag) { + trackIfSandboxed(); + entity.removeTag(tag); + } + + // ---- Glowing (MC extension) ---- + + public boolean isGlowing() { return entity.isCurrentlyGlowing(); } + public void setGlowing(boolean v) { trackIfSandboxed(); entity.setGlowingTag(v); } + + // ---- Name tag (MC extension) ---- + + public String getNameTag() { + var cn = entity.getCustomName(); + return cn != null ? cn.getString() : ""; + } + + public void setNameTag(String name) { + trackIfSandboxed(); + entity.setCustomName(net.minecraft.network.chat.Component.literal(name)); + entity.setCustomNameVisible(true); + } + + // ---- Movement queries (MC extension) ---- + + public boolean getOnGround() { return entity.onGround(); } + + public GameVector3 getEyePosition() { + var eye = entity.getEyePosition(); + return new GameVector3(eye.x, eye.y, eye.z); + } + + // ---- Health / Combat ---- + + public boolean getDestroyed() { return entity.isRemoved(); } + + public double getHp() { + LivingEntity le = asLiving(); + if (le != null) return le.getHealth(); + return getProp("hp", 100.0); + } + public void setHp(double v) { + trackIfSandboxed(); + setProp("hp", v); + LivingEntity le = asLiving(); + if (le != null) { + double max = le.getMaxHealth(); + le.setHealth((float) Math.max(0, Math.min(v, max))); + } + } + + public double getMaxHp() { + LivingEntity le = asLiving(); + if (le != null) return le.getMaxHealth(); + return getProp("maxHp", 100.0); + } + public void setMaxHp(double v) { + trackIfSandboxed(); + setProp("maxHp", v); + LivingEntity le = asLiving(); + if (le != null) { + le.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH) + .setBaseValue(v); + if (le.getHealth() > v) le.setHealth((float) v); + } + } + + public void hurt(double amount) { + LivingEntity le = asLiving(); + if (le != null) le.hurt(le.damageSources().generic(), (float) amount); + } + + public void heal(double amount) { + LivingEntity le = asLiving(); + if (le != null) le.heal((float) amount); + } + + private LivingEntity asLiving() { + return entity instanceof LivingEntity le ? le : null; + } + + private void trackIfSandboxed() { + engine.getSandbox().trackEntityModify(engine.getCurrentProject(), entity); + } + + // ---- Invulnerable (MC extension) ---- + + public boolean isInvulnerable() { return entity.isInvulnerable(); } + public void setInvulnerable(boolean v) { trackIfSandboxed(); entity.setInvulnerable(v); } + + // ---- Fire (MC extension) ---- + + public void setFire(int ticks) { + trackIfSandboxed(); + entity.setRemainingFireTicks(ticks); + } + + public void clearFire() { + entity.setRemainingFireTicks(0); + } + + // ---- Look at (MC extension) ---- + + public void lookAt(double x, double y, double z) { Box3ScriptUtils.lookAt(entity, x, y, z); } + public void lookAt(GameVector3 pos) { lookAt(pos.x, pos.y, pos.z); } + + // ---- Navigation (MC extension) ---- + + public boolean navigateTo(double x, double y, double z, double speed) { + if (entity instanceof PathfinderMob mob) { + return mob.getNavigation().moveTo(x, y, z, speed); + } + return false; + } + public boolean navigateTo(GameVector3 pos, double speed) { + return navigateTo(pos.x, pos.y, pos.z, speed); + } + + /** Set the mob's attack target. The mob will pathfind to and attack the target. */ + public void setTarget(Box3JSEntity target) { + trackIfSandboxed(); + if (entity instanceof Mob mob && target != null && target.getEntity() instanceof LivingEntity le) { + mob.setTarget(le); + } + } + + /** Clear the mob's attack target, stopping pursuit. */ + public void clearTarget() { + trackIfSandboxed(); + if (entity instanceof Mob mob) { + mob.setTarget(null); + } + } + + /** Get the mob's current attack target, or null. */ + public Box3JSEntity getTarget() { + if (entity instanceof Mob mob) { + LivingEntity target = mob.getTarget(); + return target != null ? new Box3JSEntity(target, server, engine) : null; + } + return null; + } + + /** Enable or disable the mob's AI (pathfinding, goals, etc.) */ + public void setAI(boolean enabled) { + trackIfSandboxed(); + if (entity instanceof Mob mob) { + mob.setNoAi(!enabled); + } + } + + // ---- Effects (MC extension) ---- + + public void addEffect(String effectId, int duration, int amplifier) { + addEffect(effectId, duration, amplifier, false); + } + + public void addEffect(String effectId, int duration, int amplifier, boolean hideParticles) { + trackIfSandboxed(); + LivingEntity le = asLiving(); + if (le == null) return; + Holder effect = Box3ScriptUtils.lookupMobEffect(effectId); + if (effect == null) return; + le.addEffect(new MobEffectInstance(effect, duration, amplifier, false, !hideParticles, true)); + } + + // ---- Equipment (MC extension) ---- + + public void setEquipment(String slot, String itemId) { + trackIfSandboxed(); + if (!(entity instanceof Mob mob)) return; + EquipmentSlot equipmentSlot = parseEquipmentSlot(slot); + if (equipmentSlot == null) return; + Item item = Box3ScriptUtils.lookupItem(itemId); + if (item == null) return; + mob.setItemSlot(equipmentSlot, new ItemStack(item)); + } + + // ---- Drop chances (MC extension) ---- + + public void setDropChance(String slot, double chance) { + trackIfSandboxed(); + if (!(entity instanceof Mob mob)) return; + float f = (float) Math.max(0, Math.min(1, chance)); + if ("all".equalsIgnoreCase(slot)) { + for (EquipmentSlot es : EquipmentSlot.values()) { + mob.setDropChance(es, f); + } + return; + } + EquipmentSlot es = parseEquipmentSlot(slot); + if (es != null) mob.setDropChance(es, f); + } + + // ---- Persistence (MC extension) ---- + + public void setPersistent(boolean v) { + trackIfSandboxed(); + if (entity instanceof Mob mob && v) mob.setPersistenceRequired(); + } + + // ---- Attributes (MC extension) ---- + + public double getAttribute(String attributeId) { + LivingEntity le = asLiving(); + if (le == null) return 0; + var holder = Box3ScriptUtils.lookupAttribute(attributeId); + if (holder != null) return le.getAttributeValue(holder); + return 0; + } + + public void setAttribute(String attributeId, double value) { + trackIfSandboxed(); + LivingEntity le = asLiving(); + if (le == null) return; + var holder = Box3ScriptUtils.lookupAttribute(attributeId); + if (holder != null) { + var instance = le.getAttribute(holder); + if (instance != null) instance.setBaseValue(value); + } + } + + // ---- Lifecycle ---- + + public void destroy() { + if (_onDestroyHandler != null) { + engine.callFunction(_onDestroyHandler, this); + } + entity.discard(); + engine.clearCustomProps(entity.getUUID()); + } + + /** Remove entity without triggering onDestroy callback */ + public void remove() { + entity.discard(); + engine.clearCustomProps(entity.getUUID()); + } + + public void setOnDestroy(Function handler) { + this._onDestroyHandler = handler; + } + + // ---- Custom properties ---- + + private Map props() { + return engine.getCustomProps(entity.getUUID()); + } + + @SuppressWarnings("unchecked") + private T getProp(String key, T defaultValue) { + Object v = props().get(key); + return v != null ? (T) v : defaultValue; + } + + private void setProp(String key, Object value) { + props().put(key, value); + } + + private static EquipmentSlot parseEquipmentSlot(String slot) { + return switch (slot.toLowerCase()) { + case "mainhand" -> EquipmentSlot.MAINHAND; + case "offhand" -> EquipmentSlot.OFFHAND; + case "head", "helmet", "helm" -> EquipmentSlot.HEAD; + case "chest", "chestplate" -> EquipmentSlot.CHEST; + case "legs", "leggings" -> EquipmentSlot.LEGS; + case "feet", "boots" -> EquipmentSlot.FEET; + default -> null; + }; + } + + /** Vector whose set() call syncs back to the MC entity */ + private static class LiveVec3 extends GameVector3 { + private final Consumer onSet; + + LiveVec3(Consumer onSet) { this.onSet = onSet; } + + @Override + public GameVector3 set(double x, double y, double z) { + this.x = x; this.y = y; this.z = z; + onSet.accept(this); + return this; + } + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEventBus.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEventBus.java new file mode 100644 index 00000000..d617a19d --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEventBus.java @@ -0,0 +1,140 @@ +package com.box3lab.box3js.script; + +import net.minecraft.core.BlockPos; +import org.mozilla.javascript.Function; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +class Box3JSEventBus { + + // Core callback lists — per-project keyed + final Map> tickCallbacks = new ConcurrentHashMap<>(); + final Map> joinCallbacks = new ConcurrentHashMap<>(); + final Map> leaveCallbacks = new ConcurrentHashMap<>(); + final Map> voxelDestroyCallbacks = new ConcurrentHashMap<>(); + final Map> voxelContactCallbacks = new ConcurrentHashMap<>(); + final Map> interactCallbacks = new ConcurrentHashMap<>(); + final Map> chatCallbacks = new ConcurrentHashMap<>(); + final Map> fluidEnterCallbacks = new ConcurrentHashMap<>(); + final Map> fluidLeaveCallbacks = new ConcurrentHashMap<>(); + final Map> entityContactCallbacks = new ConcurrentHashMap<>(); + final Map> entitySeparateCallbacks = new ConcurrentHashMap<>(); + final Map> blockPlaceCallbacks = new ConcurrentHashMap<>(); + final Map> entityDeathCallbacks = new ConcurrentHashMap<>(); + final Map> respawnCallbacks = new ConcurrentHashMap<>(); + final Map> blockActivateCallbacks = new ConcurrentHashMap<>(); + final Map> entityDamageCallbacks = new ConcurrentHashMap<>(); + final Map> messageCallbacks = new ConcurrentHashMap<>(); + + // Tracking state — per-project + final Map> voxelContactTracked = new ConcurrentHashMap<>(); + final Map> fluidStateTracked = new ConcurrentHashMap<>(); + final Map> entityContactPairs = new ConcurrentHashMap<>(); + final Map> playerChatHandlers = new ConcurrentHashMap<>(); + final Map> entityCustomProps = new HashMap<>(); + final Map> timers = new ConcurrentHashMap<>(); + final Map timerIdCounters = new ConcurrentHashMap<>(); + + // Per-project helpers + Map voxelContactFor(String project) { + return voxelContactTracked.computeIfAbsent(project, k -> new ConcurrentHashMap<>()); + } + Map fluidStateFor(String project) { + return fluidStateTracked.computeIfAbsent(project, k -> new ConcurrentHashMap<>()); + } + Set contactPairsFor(String project) { + return entityContactPairs.computeIfAbsent(project, k -> ConcurrentHashMap.newKeySet()); + } + Map chatHandlersFor(String project) { + return playerChatHandlers.computeIfAbsent(project, k -> new ConcurrentHashMap<>()); + } + List timersFor(String project) { + return timers.computeIfAbsent(project, k -> new ArrayList<>()); + } + int nextTimerId(String project) { + return timerIdCounters.merge(project, 1, Integer::sum); + } + + // ---- Add callbacks ---- + + void addTick(String project, Runnable cb) { add(tickCallbacks, project, cb); } + void addJoin(String project, PlayerJoinCallback cb) { add(joinCallbacks, project, cb); } + void addLeave(String project, PlayerLeaveCallback cb) { add(leaveCallbacks, project, cb); } + void addVoxelDestroy(String project, VoxelDestroyCallback cb) { add(voxelDestroyCallbacks, project, cb); } + void addVoxelContact(String project, VoxelContactCallback cb) { add(voxelContactCallbacks, project, cb); } + void addInteract(String project, InteractCallback cb) { add(interactCallbacks, project, cb); } + void addChat(String project, ChatCallback cb) { add(chatCallbacks, project, cb); } + void addFluidEnter(String project, FluidEnterCallback cb) { add(fluidEnterCallbacks, project, cb); } + void addFluidLeave(String project, FluidLeaveCallback cb) { add(fluidLeaveCallbacks, project, cb); } + void addEntityContact(String project, EntityContactCallback cb) { add(entityContactCallbacks, project, cb); } + void addEntitySeparate(String project, EntitySeparateCallback cb) { add(entitySeparateCallbacks, project, cb); } + void addBlockPlace(String project, BlockPlaceCallback cb) { add(blockPlaceCallbacks, project, cb); } + void addEntityDeath(String project, EntityDeathCallback cb) { add(entityDeathCallbacks, project, cb); } + void addRespawn(String project, PlayerRespawnCallback cb) { add(respawnCallbacks, project, cb); } + void addBlockActivate(String project, BlockActivateCallback cb) { add(blockActivateCallbacks, project, cb); } + void addEntityDamage(String project, EntityDamageCallback cb) { add(entityDamageCallbacks, project, cb); } + void addMessage(String project, MessageCallback cb) { add(messageCallbacks, project, cb); } + + private static void add(Map> map, String project, T cb) { + map.computeIfAbsent(project, k -> new CopyOnWriteArrayList<>()).add(cb); + } + + // ---- Remove one project ---- + + void removeProject(String project) { + tickCallbacks.remove(project); + joinCallbacks.remove(project); + leaveCallbacks.remove(project); + voxelDestroyCallbacks.remove(project); + voxelContactCallbacks.remove(project); + interactCallbacks.remove(project); + chatCallbacks.remove(project); + fluidEnterCallbacks.remove(project); + fluidLeaveCallbacks.remove(project); + entityContactCallbacks.remove(project); + entitySeparateCallbacks.remove(project); + blockPlaceCallbacks.remove(project); + entityDeathCallbacks.remove(project); + respawnCallbacks.remove(project); + blockActivateCallbacks.remove(project); + entityDamageCallbacks.remove(project); + messageCallbacks.remove(project); + voxelContactTracked.remove(project); + fluidStateTracked.remove(project); + entityContactPairs.remove(project); + playerChatHandlers.remove(project); + timers.remove(project); + timerIdCounters.remove(project); + } + + // ---- Clear all ---- + + void clearAll() { + tickCallbacks.clear(); + joinCallbacks.clear(); + leaveCallbacks.clear(); + voxelDestroyCallbacks.clear(); + voxelContactCallbacks.clear(); + interactCallbacks.clear(); + chatCallbacks.clear(); + fluidEnterCallbacks.clear(); + fluidLeaveCallbacks.clear(); + entityContactCallbacks.clear(); + entitySeparateCallbacks.clear(); + blockPlaceCallbacks.clear(); + entityDeathCallbacks.clear(); + respawnCallbacks.clear(); + blockActivateCallbacks.clear(); + entityDamageCallbacks.clear(); + messageCallbacks.clear(); + voxelContactTracked.clear(); + fluidStateTracked.clear(); + entityContactPairs.clear(); + playerChatHandlers.clear(); + entityCustomProps.clear(); + timers.clear(); + timerIdCounters.clear(); + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java new file mode 100644 index 00000000..6a92e151 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java @@ -0,0 +1,484 @@ +package com.box3lab.box3js.script; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ClientboundSetSubtitleTextPacket; +import net.minecraft.network.protocol.game.ClientboundSetTitleTextPacket; +import net.minecraft.network.protocol.game.ClientboundSetTitlesAnimationPacket; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.GameType; +import org.mozilla.javascript.Function; +import org.mozilla.javascript.NativeObject; +import org.mozilla.javascript.ScriptableObject; + +import java.util.Map; +import java.util.function.Consumer; + +public class Box3JSPlayer { + + private final ServerPlayer player; + private final MinecraftServer server; + private final Box3ScriptEngine engine; + + public Box3JSPlayer(ServerPlayer player, MinecraftServer server, Box3ScriptEngine engine) { + this.player = player; + this.server = server; + this.engine = engine; + } + + // ---- Info ---- + + public String getName() { return player.getGameProfile().getName(); } + public String getUserId() { return player.getUUID().toString(); } + + public int getOpLevel() { return server.getProfilePermissions(player.getGameProfile()); } + + public void setOpLevel(int level) { + trackIfSandboxed(); + if (level > 0) { + server.getPlayerList().op(player.getGameProfile()); + } else { + server.getPlayerList().deop(player.getGameProfile()); + } + } + + // ---- Appearance ---- + + public boolean getInvisible() { return player.isInvisible(); } + public void setInvisible(boolean v) { trackIfSandboxed(); player.setInvisible(v); } + + public double getScale() { return player.getScale(); } + + // ---- Movement ---- + + public double getWalkSpeed() { return player.getAttributeValue(Attributes.MOVEMENT_SPEED); } + public void setWalkSpeed(double v) { + trackIfSandboxed(); + player.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(v); + } + + public double getRunSpeed() { return player.getAttributeValue(Attributes.MOVEMENT_SPEED) * 1.3; } + public void setRunSpeed(double v) { + trackIfSandboxed(); + player.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(v / 1.3); + } + + public double getJumpPower() { return player.getAttributeValue(Attributes.JUMP_STRENGTH); } + public void setJumpPower(double v) { + trackIfSandboxed(); + player.getAttribute(Attributes.JUMP_STRENGTH).setBaseValue(v); + } + + public String getMoveState() { + if (player.getAbilities().flying) return "FLYING"; + if (player.isInWater()) return "SWIM"; + if (!player.onGround()) { + if (player.getDeltaMovement().y > 0.01) return "JUMP"; + return "FALL"; + } + return "GROUND"; + } + + public String getWalkState() { + if (player.isCrouching()) return "CROUCH"; + if (player.isSprinting()) return "RUN"; + var delta = player.getDeltaMovement(); + if (Math.abs(delta.x) > 0.01 || Math.abs(delta.z) > 0.01) return "WALK"; + return "NONE"; + } + + // ---- Fly / Spectator ---- + + public boolean getCanFly() { return player.getAbilities().mayfly; } + public void setCanFly(boolean v) { + trackIfSandboxed(); + updateAbility(a -> a.mayfly = v); + } + + public boolean getFlying() { return player.getAbilities().flying; } + public void setFlying(boolean v) { + trackIfSandboxed(); + updateAbility(a -> a.flying = v); + } + + public boolean getCollision() { + var team = server.getScoreboard().getPlayersTeam(player.getScoreboardName()); + return team == null || team.getCollisionRule() != net.minecraft.world.scores.Team.CollisionRule.NEVER; + } + public void setCollision(boolean enabled) { + trackIfSandboxed(); + var team = server.getScoreboard().getPlayersTeam(player.getScoreboardName()); + if (team != null) { + team.setCollisionRule(enabled + ? net.minecraft.world.scores.Team.CollisionRule.ALWAYS + : net.minecraft.world.scores.Team.CollisionRule.NEVER); + } + } + + public boolean getSpectator() { return player.isSpectator(); } + + public double getFlySpeed() { return player.getAbilities().getFlyingSpeed(); } + public void setFlySpeed(double v) { + trackIfSandboxed(); + updateAbility(a -> a.setFlyingSpeed((float) v)); + } + + // ---- Game Mode ---- + + public String getGameMode() { return player.gameMode.getGameModeForPlayer().getName(); } + public void setGameMode(Object v) { + trackIfSandboxed(); + GameType type; + if (v instanceof Number n) { + type = GameType.byId(n.intValue()); + } else { + type = GameType.byName(v.toString()); + } + if (type != null) player.setGameMode(type); + } + + // ---- Dimension (MC extension) ---- + + public String getDimension() { + return player.level().dimension().location().toString(); + } + public void setDimension(String dimId) { + ResourceLocation rl = ResourceLocation.tryParse(dimId); + if (rl == null) return; + ServerLevel target = server.getLevel(ResourceKey.create(Registries.DIMENSION, rl)); + if (target != null) { + player.teleportTo(target, player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()); + } + } + + // ---- Disable Fly ---- + + public boolean getDisableFly() { return getProp("disableFly", false); } + public void setDisableFly(boolean v) { + trackIfSandboxed(); + setProp("disableFly", v); + if (v) { player.getAbilities().mayfly = false; player.getAbilities().flying = false; } + } + + // ---- Camera ---- + + public String getCameraMode() { return getProp("cameraMode", "FPS"); } + public void setCameraMode(String v) { + setProp("cameraMode", v); + if ("FPS".equals(v)) player.setCamera(null); + } + + public Object getCameraEntity() { + return getProp("cameraEntity", null); + } + public void setCameraEntity(Object v) { + setProp("cameraEntity", v); + if (v == null) { + setProp("cameraMode", "FPS"); + player.setCamera(null); + } else if (v instanceof Box3JSEntity be) { + setProp("cameraMode", "FOLLOW"); + player.setCamera(be.getEntity()); + } + } + + public double getCameraPitch() { return player.getXRot(); } + public void setCameraPitch(double v) { player.setXRot((float) v); } + + public double getCameraYaw() { return player.getYRot(); } + public void setCameraYaw(double v) { player.setYRot((float) v); } + + public GameVector3 getFacingDirection() { + var look = player.getLookAngle(); + return new GameVector3(look.x, look.y, look.z); + } + + public GameVector3 getCameraTarget() { + var look = player.getLookAngle(); + var eye = player.getEyePosition(); + return new GameVector3(eye.x + look.x * 5.0, eye.y + look.y * 5.0, eye.z + look.z * 5.0); + } + + // ---- Respawn ---- + + public void setRespawnPoint(GameVector3 pos) { + player.setRespawnPosition( + player.level().dimension(), + new BlockPos((int) pos.x, (int) pos.y, (int) pos.z), + 0, true, false); + } + + public void respawn() { + if (player.isDeadOrDying()) { + player.respawn(); + } + } + + // ---- Kick ---- + + public void kick() { kick("Kicked"); } + + public void kick(String reason) { + player.connection.disconnect(Component.literal(reason)); + } + + // ---- Messaging ---- + + public void directMessage(String msg) { + player.sendSystemMessage(Component.literal(msg)); + } + + public void actionBar(String message) { + player.displayClientMessage(Component.literal(message), true); + } + + public void title(String title, String subtitle) { + title(title, subtitle, 10, 70, 20); + } + public void title(String title, String subtitle, int fadeIn, int stay, int fadeOut) { + player.connection.send(new ClientboundSetTitlesAnimationPacket(fadeIn, stay, fadeOut)); + player.connection.send(new ClientboundSetTitleTextPacket(Component.literal(title))); + if (subtitle != null && !subtitle.isEmpty()) { + player.connection.send(new ClientboundSetSubtitleTextPacket(Component.literal(subtitle))); + } + } + + public void teleport(GameVector3 pos) { + player.teleportTo(pos.x, pos.y, pos.z); + } + + public Object dialog(NativeObject config) { + String content = config.containsKey("content") ? config.get("content").toString() : ""; + Object optionsVal = config.containsKey("options") ? config.get("options") : null; + + String[] opts; + if (optionsVal instanceof NativeObject optObj && optObj.containsKey("length")) { + int len = ((Number) optObj.get("length")).intValue(); + opts = new String[len]; + for (int i = 0; i < len; i++) { + opts[i] = String.valueOf(optObj.get(i)); + } + } else { + opts = new String[]{"OK"}; + } + + player.sendSystemMessage(Component.literal(content)); + + NativeObject result = new NativeObject(); + ScriptableObject.putProperty(result, "index", 0); + ScriptableObject.putProperty(result, "value", opts[0]); + return result; + } + + // ---- Chat (player-level) ---- + + public void onChat(Function handler) { + engine.setPlayerChatHandler(player.getUUID(), handler); + } + + // ---- Link ---- + + public void link(String href) { + var comp = Component.literal(href) + .withStyle(style -> style + .withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, href)) + .withUnderlined(true) + .withColor(net.minecraft.network.chat.TextColor.fromRgb(0x5555FF))); + player.sendSystemMessage(comp); + } + + // ---- Tab list (MC extension) ---- + + public void setPlayerListName(String name) { + try { + java.lang.reflect.Field f = net.minecraft.world.entity.player.Player.class.getDeclaredField("displayName"); + f.setAccessible(true); + f.set(player, Component.literal(name)); + server.getPlayerList().broadcastAll( + new net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket( + java.util.EnumSet.of(net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME), + java.util.List.of(player))); + } catch (Exception ignored) {} + } + + // ---- Look at (MC extension) ---- + + public void lookAt(double x, double y, double z) { Box3ScriptUtils.lookAt(player, x, y, z); } + public void lookAt(GameVector3 pos) { lookAt(pos.x, pos.y, pos.z); } + + // ---- Command ---- + + public void runCommand(String cmd) { + net.minecraft.commands.CommandSourceStack source = player.createCommandSourceStack(); + server.getCommands().performPrefixedCommand(source, cmd); + } + + // ---- Health ---- + + public double getHp() { + return player.getHealth(); + } + public void setHp(double v) { + trackIfSandboxed(); + player.setHealth((float) Math.min(v, player.getMaxHealth())); + } + + public double getMaxHp() { + return player.getMaxHealth(); + } + public void setMaxHp(double v) { + trackIfSandboxed(); + player.getAttribute(Attributes.MAX_HEALTH).setBaseValue(v); + if (player.getHealth() > v) player.setHealth((float) v); + } + + // ---- XP / Food ---- + + public int getXp() { return player.experienceLevel; } + public void setXp(int v) { player.experienceLevel = v; } + + public void addExperienceLevels(int levels) { + player.experienceLevel += levels; + } + + public int getFood() { return player.getFoodData().getFoodLevel(); } + public void setFood(int v) { player.getFoodData().setFoodLevel(v); } + + public float getSaturation() { return player.getFoodData().getSaturationLevel(); } + public void setSaturation(float v) { player.getFoodData().setSaturation(v); } + + // ---- Inventory ---- + + public void giveItem(String itemId, int count) { + ItemStack stack = makeItemStack(itemId, count, null); + if (stack != null) player.getInventory().add(stack); + } + + public void giveEnchantedItem(String itemId, int count, NativeObject enchants) { + ItemStack stack = makeItemStack(itemId, count, enchants); + if (stack != null) player.getInventory().add(stack); + } + + public void giveNamedItem(String itemId, int count, String customName, Object lore) { + ItemStack stack = makeItemStack(itemId, count, null); + if (stack == null) return; + if (customName != null && !customName.isEmpty()) { + stack.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME, + Component.literal(customName)); + } + if (lore instanceof NativeObject lo) { + var lines = new java.util.ArrayList(); + for (int i = 0; ; i++) { + Object line = lo.get(i); + if (line == null || line == org.mozilla.javascript.UniqueTag.NOT_FOUND) break; + lines.add(Component.literal(line.toString())); + } + if (!lines.isEmpty()) { + stack.set(net.minecraft.core.component.DataComponents.LORE, + new net.minecraft.world.item.component.ItemLore(lines)); + } + } + player.getInventory().add(stack); + } + + public Object getHeldItem() { + ItemStack stack = player.getMainHandItem(); + NativeObject result = new NativeObject(); + if (stack.isEmpty()) { + ScriptableObject.putProperty(result, "id", "minecraft:air"); + ScriptableObject.putProperty(result, "count", 0); + return result; + } + ResourceLocation key = BuiltInRegistries.ITEM.getKey(stack.getItem()); + ScriptableObject.putProperty(result, "id", key.toString()); + ScriptableObject.putProperty(result, "count", stack.getCount()); + return result; + } + + public void clearInventory() { + player.getInventory().clearContent(); + } + + // ---- Effects ---- + + public void addEffect(String effectId, int duration, int amplifier) { + addEffect(effectId, duration, amplifier, false); + } + + public void addEffect(String effectId, int duration, int amplifier, boolean hideParticles) { + trackIfSandboxed(); + var effect = Box3ScriptUtils.lookupMobEffect(effectId); + if (effect != null) { + player.addEffect(new MobEffectInstance(effect, duration, amplifier, false, !hideParticles, true)); + } + } + + public void clearEffects() { + trackIfSandboxed(); + player.removeAllEffects(); + } + + // ---- Sound ---- + + public void playSound(String path, double volume, double pitch) { + var sound = Box3ScriptUtils.lookupSoundEvent(path); + if (sound != null) { + player.playNotifySound(sound.value(), net.minecraft.sounds.SoundSource.PLAYERS, (float) volume, (float) pitch); + } + } + + // ---- Custom properties ---- + + private void trackIfSandboxed() { + engine.getSandbox().trackPlayer(engine.getCurrentProject(), player); + } + + private Map props() { + return engine.getCustomProps(player.getUUID()); + } + + @SuppressWarnings("unchecked") + private T getProp(String key, T defaultValue) { + Object v = props().get(key); + return v != null ? (T) v : defaultValue; + } + + private void setProp(String key, Object value) { + props().put(key, value); + } + + private void updateAbility(Consumer updater) { + updater.accept(player.getAbilities()); + player.onUpdateAbilities(); + } + + private ItemStack makeItemStack(String itemId, int count, NativeObject enchants) { + var item = Box3ScriptUtils.lookupItem(itemId); + if (item == null) return null; + ItemStack stack = new ItemStack(item, Math.max(1, Math.min(count, 64))); + if (enchants != null) { + var enchRegistry = player.server.registryAccess().registryOrThrow(Registries.ENCHANTMENT); + for (Object key : enchants.keySet()) { + String enchId = key.toString(); + int level = ((Number) enchants.get(key)).intValue(); + ResourceLocation enchRl = ResourceLocation.tryParse(enchId); + if (enchRl == null) continue; + var holder = enchRegistry.getHolder(enchRl); + if (holder.isPresent()) { + stack.enchant(holder.get(), level); + } + } + } + return stack; + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSQuery.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSQuery.java new file mode 100644 index 00000000..f478ed53 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSQuery.java @@ -0,0 +1,150 @@ +package com.box3lab.box3js.script; + +import org.mozilla.javascript.NativeObject; +import org.mozilla.javascript.ScriptableObject; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.List; + +class Box3JSQuery { + + private final MinecraftServer server; + private final Box3ScriptEngine engine; + + Box3JSQuery(MinecraftServer server, Box3ScriptEngine engine) { + this.server = server; + this.engine = engine; + } + + List querySelectorAll(String selector) { + List result = new ArrayList<>(); + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + Box3JSEntity e = new Box3JSEntity(player, server, engine); + if (matchesSelector(e, selector)) result.add(e); + } + return result; + } + + Box3JSEntity querySelector(String selector) { + List all = querySelectorAll(selector); + return all.isEmpty() ? null : all.get(0); + } + + private static boolean matchesSelector(Box3JSEntity entity, String selector) { + if (selector.equals("*") || selector.equals("player")) return entity.isPlayer(); + if (selector.startsWith("#")) return selector.substring(1).equals(entity.getId()); + if (selector.startsWith(".")) return entity.hasTag(selector.substring(1)); + return false; + } + + Object raycast(GameVector3 origin, GameVector3 direction) { + return raycast(origin, direction, 5.0); + } + + Object raycast(GameVector3 origin, GameVector3 direction, double maxDistance) { + ServerLevel level = server.overworld(); + Vec3 start = new Vec3(origin.x, origin.y, origin.z); + double len = Math.sqrt(direction.x * direction.x + direction.y * direction.y + direction.z * direction.z); + if (len < 0.0001) { + NativeObject result = new NativeObject(); + ScriptableObject.putProperty(result, "hit", false); + return result; + } + Vec3 dir = new Vec3(direction.x / len, direction.y / len, direction.z / len); + Vec3 end = start.add(dir.scale(maxDistance)); + ClipContext ctx = new ClipContext(start, end, ClipContext.Block.OUTLINE, ClipContext.Fluid.NONE, net.minecraft.world.phys.shapes.CollisionContext.empty()); + BlockHitResult blockHit = level.clip(ctx); + AABB searchBox = new AABB(start, end).inflate(1.0); + Entity closestEntity = null; + Vec3 entityHitPos = null; + double closestEntDistSqr = maxDistance * maxDistance; + for (Entity e : level.getEntities((Entity) null, searchBox, e2 -> true)) { + var hit = e.getBoundingBox().clip(start, end); + if (hit.isPresent()) { + double dSqr = start.distanceToSqr(hit.get()); + if (dSqr < closestEntDistSqr) { + closestEntDistSqr = dSqr; + closestEntity = e; + entityHitPos = hit.get(); + } + } + } + double blockDistSqr = blockHit.getType() != HitResult.Type.MISS + ? start.distanceToSqr(blockHit.getLocation()) : Double.MAX_VALUE; + NativeObject result = new NativeObject(); + if (closestEntity != null && closestEntDistSqr < blockDistSqr) { + ScriptableObject.putProperty(result, "hit", true); + ScriptableObject.putProperty(result, "x", entityHitPos.x); + ScriptableObject.putProperty(result, "y", entityHitPos.y); + ScriptableObject.putProperty(result, "z", entityHitPos.z); + ScriptableObject.putProperty(result, "normalX", 0); + ScriptableObject.putProperty(result, "normalY", 0); + ScriptableObject.putProperty(result, "normalZ", 0); + ScriptableObject.putProperty(result, "distance", Math.sqrt(closestEntDistSqr)); + ScriptableObject.putProperty(result, "entity", new Box3JSEntity(closestEntity, server, engine)); + } else if (blockHit.getType() != HitResult.Type.MISS) { + Vec3 pos = blockHit.getLocation(); + ScriptableObject.putProperty(result, "hit", true); + ScriptableObject.putProperty(result, "x", pos.x); + ScriptableObject.putProperty(result, "y", pos.y); + ScriptableObject.putProperty(result, "z", pos.z); + Direction face = blockHit.getDirection(); + ScriptableObject.putProperty(result, "normalX", face.getStepX()); + ScriptableObject.putProperty(result, "normalY", face.getStepY()); + ScriptableObject.putProperty(result, "normalZ", face.getStepZ()); + ScriptableObject.putProperty(result, "distance", Math.sqrt(blockDistSqr)); + ScriptableObject.putProperty(result, "entity", null); + BlockPos bp = blockHit.getBlockPos(); + ScriptableObject.putProperty(result, "voxel", engine.getVoxelsBinding().getId(level.getBlockState(bp))); + } else { + ScriptableObject.putProperty(result, "hit", false); + } + return result; + } + + List entitiesInArea(GameVector3 pos1, GameVector3 pos2) { + AABB aabb = new AABB(pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z); + List result = new ArrayList<>(); + for (Entity e : server.overworld().getEntities((Entity) null, aabb, e2 -> true)) { + result.add(new Box3JSEntity(e, server, engine)); + } + return result; + } + + List entitiesInRadius(double x, double y, double z, double radius) { + AABB aabb = new AABB(x - radius, y - radius, z - radius, x + radius, y + radius, z + radius); + List result = new ArrayList<>(); + for (Entity e : server.overworld().getEntities((Entity) null, aabb, e2 -> true)) { + result.add(new Box3JSEntity(e, server, engine)); + } + return result; + } + + List entitiesInRadius(GameVector3 pos, double radius) { + return entitiesInRadius(pos.x, pos.y, pos.z, radius); + } + + String getBiome(int x, int y, int z) { + Holder biome = server.overworld().getBiome(new BlockPos(x, y, z)); + var key = biome.unwrapKey(); + return key.map(k -> k.location().toString()).orElse("unknown"); + } + + String getBiome(GameVector3 pos) { + return getBiome((int) pos.x, (int) pos.y, (int) pos.z); + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSScoreboard.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSScoreboard.java new file mode 100644 index 00000000..127e68cc --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSScoreboard.java @@ -0,0 +1,121 @@ +package com.box3lab.box3js.script; + +import org.mozilla.javascript.NativeObject; +import org.mozilla.javascript.ScriptableObject; + +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.scores.DisplaySlot; +import net.minecraft.world.scores.Objective; +import net.minecraft.world.scores.ScoreAccess; +import net.minecraft.world.scores.ScoreHolder; +import net.minecraft.world.scores.Scoreboard; +import net.minecraft.world.scores.criteria.ObjectiveCriteria; + +import java.util.*; + +class Box3JSScoreboard { + + private final MinecraftServer server; + private final Map> projectObjectives = new HashMap<>(); + + Box3JSScoreboard(MinecraftServer server) { + this.server = server; + } + + void addScoreboard(String project, String name) { addScoreboard(project, name, "dummy"); } + + void addScoreboard(String project, String name, String criteria) { + Scoreboard sb = server.getScoreboard(); + if (sb.getObjective(name) != null) return; + ObjectiveCriteria crit = "dummy".equals(criteria) || criteria == null + ? ObjectiveCriteria.DUMMY + : ObjectiveCriteria.byName(criteria).orElse(ObjectiveCriteria.DUMMY); + sb.addObjective(name, crit, Component.literal(name), ObjectiveCriteria.RenderType.INTEGER, false, null); + projectObjectives.computeIfAbsent(project, k -> new HashSet<>()).add(name); + } + + void removeScoreboard(String name) { + Scoreboard sb = server.getScoreboard(); + Objective obj = sb.getObjective(name); + if (obj != null) { + sb.removeObjective(obj); + for (Set set : projectObjectives.values()) set.remove(name); + } + } + + void setScore(Object entityOrName, String objectiveName, int value) { + Scoreboard sb = server.getScoreboard(); + Objective obj = sb.getObjective(objectiveName); + if (obj == null) return; + String name = Box3ScriptUtils.resolveScoreName(entityOrName); + if (name == null) return; + sb.getOrCreatePlayerScore(ScoreHolder.forNameOnly(name), obj).set(value); + } + + int getScore(Object entityOrName, String objectiveName) { + Scoreboard sb = server.getScoreboard(); + Objective obj = sb.getObjective(objectiveName); + if (obj == null) return 0; + String name = Box3ScriptUtils.resolveScoreName(entityOrName); + if (name == null) return 0; + ScoreAccess access = sb.getOrCreatePlayerScore(ScoreHolder.forNameOnly(name), obj); + return access.get(); + } + + void showScoreboard(String slot, String objectiveName) { + Scoreboard sb = server.getScoreboard(); + DisplaySlot displaySlot = parseSlot(slot); + Objective obj = sb.getObjective(objectiveName); + sb.setDisplayObjective(displaySlot, obj); + } + + void hideScoreboard(String slot) { + Scoreboard sb = server.getScoreboard(); + sb.setDisplayObjective(parseSlot(slot), null); + } + + List listScores(String objectiveName) { + List result = new ArrayList<>(); + Scoreboard sb = server.getScoreboard(); + Objective obj = sb.getObjective(objectiveName); + if (obj == null) return result; + for (var entry : sb.listPlayerScores(obj)) { + NativeObject m = new NativeObject(); + ScriptableObject.putProperty(m, "name", entry.owner()); + ScriptableObject.putProperty(m, "score", entry.value()); + result.add(m); + } + return result; + } + + void removeProject(String project) { + Set objectives = projectObjectives.remove(project); + if (objectives != null) { + Scoreboard sb = server.getScoreboard(); + for (String name : objectives) { + Objective obj = sb.getObjective(name); + if (obj != null) sb.removeObjective(obj); + } + } + } + + void resetAll() { + Scoreboard sb = server.getScoreboard(); + for (Set objectives : projectObjectives.values()) { + for (String name : objectives) { + Objective obj = sb.getObjective(name); + if (obj != null) sb.removeObjective(obj); + } + } + projectObjectives.clear(); + } + + private static DisplaySlot parseSlot(String slot) { + return switch (slot.toLowerCase()) { + case "list" -> DisplaySlot.LIST; + case "belowname", "below_name" -> DisplaySlot.BELOW_NAME; + default -> DisplaySlot.SIDEBAR; + }; + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSStorage.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSStorage.java new file mode 100644 index 00000000..c340f82a --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSStorage.java @@ -0,0 +1,322 @@ +package com.box3lab.box3js.script; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import org.mozilla.javascript.Function; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class Box3JSStorage { + + private static final Gson GSON = new Gson(); + private static final Type MAP_TYPE = new TypeToken>() {}.getType(); + + private final Path baseDir; + private final Box3ScriptEngine engine; + private final Map> cache = new ConcurrentHashMap<>(); + + public Box3JSStorage(Path configDir, Box3ScriptEngine engine) { + this.baseDir = configDir.resolve("box3").resolve("storage"); + this.engine = engine; + try { Files.createDirectories(baseDir); } catch (IOException ignored) {} + } + + // ---- GameStorage ---- + + public String getKey() { return ""; } + + public GameDataStorage getDataStorage(String name) { + return new GameDataStorage(resolveName(name)); + } + + /** Shared storage accessible by all projects. */ + public GameDataStorage getGroupStorage(String name) { + return new GameDataStorage("__shared__/" + name); + } + + /** Prefix with project namespace if running inside a project. */ + private String resolveName(String name) { + String project = engine.getCurrentProject(); + return project != null ? project + "/" + name : name; + } + + // ---- ValueEntry ---- + + private static class ValueEntry { + Object value; + long updateTime; + long createTime; + String version; + + ValueEntry(Object value, long createTime) { + this.value = value; + this.createTime = createTime; + this.updateTime = createTime; + this.version = Long.toHexString(createTime) + "-" + Integer.toHexString(new Random().nextInt()); + } + } + + // ---- ReturnValue ---- + + public static class ReturnValue { + public String key; + public Object value; + public double updateTime; + public double createTime; + public String version; + + ReturnValue(String key, ValueEntry entry) { + this.key = key; + this.value = entry.value; + this.updateTime = entry.updateTime; + this.createTime = entry.createTime; + this.version = entry.version; + } + } + + // ---- QueryList ---- + + public static class QueryList { + public boolean isLastPage; + private final List all; + private final int pageSize; + private int cursor; + + QueryList(List all, int pageSize, int cursor) { + this.all = all; + this.pageSize = pageSize; + this.cursor = Math.max(0, cursor); + this.isLastPage = this.cursor >= all.size(); + } + + public ReturnValue[] getCurrentPage() { + int end = Math.min(cursor + pageSize, all.size()); + if (cursor >= all.size()) return new ReturnValue[0]; + return all.subList(cursor, end).toArray(new ReturnValue[0]); + } + + public void nextPage() { + cursor += pageSize; + isLastPage = cursor >= all.size(); + } + } + + // ---- GameDataStorage ---- + + public class GameDataStorage { + + private final String name; + private final Path path; + private final Map data; + + GameDataStorage(String name) { + this.name = name; + String[] parts = name.split("/"); + Path dir = baseDir; + for (int i = 0; i < parts.length - 1; i++) { + String seg = sanitize(parts[i]); + if (!seg.isEmpty()) dir = dir.resolve(seg); + } + String file = sanitize(parts[parts.length - 1]); + if (file.isEmpty()) file = "default"; + this.path = dir.resolve(file + ".json"); + this.data = cache.computeIfAbsent(path, p -> { + if (Files.exists(p)) { + try { + String json = Files.readString(p); + Map map = GSON.fromJson(json, MAP_TYPE); + return map != null ? Collections.synchronizedMap(new LinkedHashMap<>(map)) + : Collections.synchronizedMap(new LinkedHashMap<>()); + } catch (IOException e) { + return Collections.synchronizedMap(new LinkedHashMap<>()); + } + } + return Collections.synchronizedMap(new LinkedHashMap<>()); + }); + } + + private String sanitize(String s) { + return s.replaceAll("[^a-zA-Z0-9_.\\-]", "_"); + } + + public String getKey() { return name; } + + // ---- Persist ---- + + private void persist() { + try { + Files.createDirectories(path.getParent()); + Files.writeString(path, GSON.toJson(data)); + } catch (IOException ignored) {} + } + + // ---- Public API ---- + + public void set(String key, Object value) { + if (key == null) return; + long now = System.currentTimeMillis(); + synchronized (data) { + ValueEntry existing = data.get(key); + if (existing != null) { + existing.value = value; + existing.updateTime = now; + existing.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); + } else { + data.put(key, new ValueEntry(value, now)); + } + persist(); + } + } + + public Object get(String key) { + if (key == null) return null; + synchronized (data) { + ValueEntry entry = data.get(key); + return entry != null ? entry.value : null; + } + } + + public String[] keys() { + synchronized (data) { + return data.keySet().toArray(new String[0]); + } + } + + public void update(String key, Function handler) { + if (key == null || handler == null) return; + synchronized (data) { + ValueEntry entry = data.get(key); + if (entry == null) return; + long now = System.currentTimeMillis(); + entry.value = engine.callFunction(handler, entry.value); + entry.updateTime = now; + entry.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); + persist(); + } + } + + public Object remove(String key) { + if (key == null) return null; + synchronized (data) { + ValueEntry entry = data.remove(key); + if (entry != null) { + persist(); + return entry.value; + } + } + return null; + } + + public double increment(String key, double value) { + if (key == null) return 0; + double delta = Double.isNaN(value) ? 1.0 : value; + long now = System.currentTimeMillis(); + synchronized (data) { + ValueEntry entry = data.get(key); + if (entry != null) { + if (entry.value instanceof Number n) { + entry.value = n.doubleValue() + delta; + } else { + entry.value = delta; + } + entry.updateTime = now; + entry.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); + } else { + entry = new ValueEntry(delta, now); + data.put(key, entry); + } + persist(); + return ((Number) entry.value).doubleValue(); + } + } + + public double increment(String key) { + return increment(key, 1.0); + } + + public QueryList list(Map options) { + List results; + synchronized (data) { + results = new ArrayList<>(); + for (Map.Entry e : data.entrySet()) { + results.add(new ReturnValue(e.getKey(), e.getValue())); + } + } + + int cursor = 0; + int pageSize = 100; + boolean ascending = false; + boolean doSort = false; + Double max = null, min = null; + String constraintTarget = null; + + if (options != null) { + if (options.get("cursor") instanceof Number n) cursor = n.intValue(); + if (options.get("pageSize") instanceof Number n) { + pageSize = Math.max(1, Math.min(100, n.intValue())); + } + if (options.containsKey("ascending")) { + doSort = true; + ascending = Boolean.TRUE.equals(options.get("ascending")); + } + if (options.get("max") instanceof Number n) max = n.doubleValue(); + if (options.get("min") instanceof Number n) min = n.doubleValue(); + if (options.get("constraintTarget") instanceof String s) constraintTarget = s; + } + + final String target = constraintTarget; + final boolean asc = ascending; + + if (doSort) { + results.sort((a, b) -> { + double va = extractSortValue(a.value, target); + double vb = extractSortValue(b.value, target); + int cmp = Double.compare(va, vb); + return asc ? cmp : -cmp; + }); + } + + if (max != null || min != null) { + List filtered = new ArrayList<>(); + for (ReturnValue rv : results) { + double v = extractSortValue(rv.value, target); + if (min != null && v < min) continue; + if (max != null && v > max) continue; + filtered.add(rv); + } + results = filtered; + } + + return new QueryList(results, pageSize, Math.max(0, cursor)); + } + + private double extractSortValue(Object value, String target) { + if (target == null || target.isEmpty()) { + if (value instanceof Number n) return n.doubleValue(); + return 0; + } + Object current = value; + for (String part : target.split("\\.")) { + if (current instanceof Map m) { + current = m.get(part); + } else { + return 0; + } + } + if (current instanceof Number n) return n.doubleValue(); + return 0; + } + + public void destroy() { + synchronized (data) { + cache.remove(path); + try { Files.deleteIfExists(path); } catch (IOException ignored) {} + } + } + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSTeam.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSTeam.java new file mode 100644 index 00000000..07349b3d --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSTeam.java @@ -0,0 +1,84 @@ +package com.box3lab.box3js.script; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.scores.PlayerTeam; +import net.minecraft.world.scores.Scoreboard; + +import java.util.*; + +class Box3JSTeam { + + private final MinecraftServer server; + private final Map> projectTeams = new HashMap<>(); + + Box3JSTeam(MinecraftServer server) { + this.server = server; + } + + void createTeam(String project, String name, String colorName) { + Scoreboard sb = server.getScoreboard(); + if (sb.getPlayerTeam(name) != null) return; + PlayerTeam team = sb.addPlayerTeam(name); + ChatFormatting fmt = ChatFormatting.getByName(colorName); + if (fmt != null) { + team.setColor(fmt); + team.setDisplayName(Component.literal(name)); + } + projectTeams.computeIfAbsent(project, k -> new HashSet<>()).add(name); + } + + void removeTeam(String name) { + Scoreboard sb = server.getScoreboard(); + PlayerTeam team = sb.getPlayerTeam(name); + if (team != null) { + sb.removePlayerTeam(team); + for (Set set : projectTeams.values()) set.remove(name); + } + } + + void joinTeam(Object entityOrName, String teamName) { + Scoreboard sb = server.getScoreboard(); + PlayerTeam team = sb.getPlayerTeam(teamName); + if (team == null) return; + String name = Box3ScriptUtils.resolveScoreName(entityOrName); + if (name != null) sb.addPlayerToTeam(name, team); + } + + void leaveTeam(Object entityOrName) { + Scoreboard sb = server.getScoreboard(); + String name = Box3ScriptUtils.resolveScoreName(entityOrName); + if (name != null) sb.removePlayerFromTeam(name); + } + + String getTeamOf(Object entityOrName) { + Scoreboard sb = server.getScoreboard(); + String name = Box3ScriptUtils.resolveScoreName(entityOrName); + if (name == null) return null; + PlayerTeam team = sb.getPlayersTeam(name); + return team != null ? team.getName() : null; + } + + void removeProject(String project) { + Set teams = projectTeams.remove(project); + if (teams != null) { + Scoreboard sb = server.getScoreboard(); + for (String name : teams) { + PlayerTeam team = sb.getPlayerTeam(name); + if (team != null) sb.removePlayerTeam(team); + } + } + } + + void resetAll() { + Scoreboard sb = server.getScoreboard(); + for (Set teams : projectTeams.values()) { + for (String name : teams) { + PlayerTeam team = sb.getPlayerTeam(name); + if (team != null) sb.removePlayerTeam(team); + } + } + projectTeams.clear(); + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSVoxels.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSVoxels.java new file mode 100644 index 00000000..36236f97 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSVoxels.java @@ -0,0 +1,366 @@ +package com.box3lab.box3js.script; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.HorizontalDirectionalBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.DirectionProperty; + +import java.util.*; + +public class Box3JSVoxels { + + static final int ROTATION_MULTIPLIER = 16384; + + private static final Direction[] ROTATION_TO_DIRECTION = { + Direction.SOUTH, // 0 = 0° + Direction.WEST, // 1 = 90° + Direction.NORTH, // 2 = 180° + Direction.EAST // 3 = 270° + }; + + private static final Map DIRECTION_TO_ROTATION = Map.of( + Direction.SOUTH, 0, + Direction.WEST, 1, + Direction.NORTH, 2, + Direction.EAST, 3 + ); + + private final MinecraftServer server; + private final Box3ScriptSandbox sandbox; + private final Map nameToId = new HashMap<>(); + private final Map idToName = new HashMap<>(); + private final Map resourceToBlock = new HashMap<>(); + private final Map blockToId = new HashMap<>(); + + // Public fields for JS access matching Box3 API naming + public final GameVector3 shape; + public final String[] VoxelTypes; + + public Box3JSVoxels(MinecraftServer server, Box3ScriptSandbox sandbox) { + this.server = server; + this.sandbox = sandbox; + + nameToId.put("air", 0); + idToName.put(0, "air"); + + List types = new ArrayList<>(); + types.add("air"); + + int nextId = 1; + for (var entry : BuiltInRegistries.BLOCK.entrySet()) { + Block block = entry.getValue(); + if (block == Blocks.AIR) continue; + ResourceLocation key = entry.getKey().location(); + String fullName = key.toString(); + String path = key.getPath(); + + nameToId.put(fullName, nextId); + if (!fullName.equals(path)) nameToId.put(path, nextId); + idToName.put(nextId, fullName); + resourceToBlock.put(fullName, block); + resourceToBlock.put(path.toLowerCase(Locale.ROOT), block); + blockToId.put(block, nextId); + types.add(fullName); + nextId++; + } + this.VoxelTypes = types.toArray(new String[0]); + + ServerLevel level = server.overworld(); + int h = level != null ? level.getMaxBuildHeight() : 256; + this.shape = new GameVector3(h, h, h); + } + + // ---- Name / ID mapping ---- + + public int id(String name) { + if (name == null || name.equalsIgnoreCase("air")) return 0; + Integer id = nameToId.get(name); + if (id != null) return id; + // Try vanilla block by ResourceLocation + Block block = Box3ScriptUtils.lookupBlock(name); + if (block != null && block != Blocks.AIR) { + Integer foundId = blockToId.get(block); + if (foundId != null) return foundId; + } + return 0; + } + + public String name(int id) { + if (id == 0) return "air"; + int baseId = id % ROTATION_MULTIPLIER; + String n = idToName.get(baseId); + if (n != null) return n; + return "air"; + } + + // ---- Write ---- + + /** setVoxel(x, y, z, voxel: number|string): number */ + public int setVoxel(int x, int y, int z, Object voxel) { + return setVoxel(x, y, z, voxel, 0); + } + /** setVoxel(pos, voxel): number */ + public int setVoxel(GameVector3 pos, Object voxel) { + return setVoxel((int) pos.x, (int) pos.y, (int) pos.z, voxel, 0); + } + + /** setVoxel(x, y, z, voxel: number|string, rotation?: number|string): number */ + public int setVoxel(int x, int y, int z, Object voxel, Object rotation) { + ServerLevel level = server.overworld(); + BlockPos pos = new BlockPos(x, y, z); + + if (sandbox != null) sandbox.trackBlock(Box3ScriptEngine.get().getCurrentProject(), pos); + + if (voxel == null) return 0; + + // Resolve "air" or 0 → remove block + if (isAir(voxel)) { + level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); + return 0; + } + + Block block = resolveBlock(voxel); + if (block == null) return 0; + + int rot = coerceRotation(rotation); + BlockState state = applyRotation(block.defaultBlockState(), rot); + level.setBlock(pos, state, 3); + + Integer baseId = blockToId.get(block); + return baseId != null ? rot * ROTATION_MULTIPLIER + baseId : 0; + } + /** setVoxel(pos, voxel, rotation): number */ + public int setVoxel(GameVector3 pos, Object voxel, Object rotation) { + return setVoxel((int) pos.x, (int) pos.y, (int) pos.z, voxel, rotation); + } + + /** fillVoxel(x1, y1, z1, x2, y2, z2, voxel): void — fill a region */ + public void fillVoxel(int x1, int y1, int z1, int x2, int y2, int z2, Object voxel) { + int minX = Math.min(x1, x2), maxX = Math.max(x1, x2); + int minY = Math.min(y1, y2), maxY = Math.max(y1, y2); + int minZ = Math.min(z1, z2), maxZ = Math.max(z1, z2); + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + for (int z = minZ; z <= maxZ; z++) { + setVoxel(x, y, z, voxel); + } + } + } + } + /** fillVoxel(pos1, pos2, voxel): void */ + public void fillVoxel(GameVector3 pos1, GameVector3 pos2, Object voxel) { + fillVoxel((int) pos1.x, (int) pos1.y, (int) pos1.z, (int) pos2.x, (int) pos2.y, (int) pos2.z, voxel); + } + + /** countVoxel(x1, y1, z1, x2, y2, z2, voxel): number — count matching blocks in region */ + public int countVoxel(int x1, int y1, int z1, int x2, int y2, int z2, Object voxel) { + var targetBlock = resolveBlock(voxel); + if (targetBlock == null) return 0; + + int minX = Math.min(x1, x2), maxX = Math.max(x1, x2); + int minY = Math.min(y1, y2), maxY = Math.max(y1, y2); + int minZ = Math.min(z1, z2), maxZ = Math.max(z1, z2); + + int count = 0; + var level = server.overworld(); + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + for (int z = minZ; z <= maxZ; z++) { + var state = level.getBlockState(new BlockPos(x, y, z)); + if (state.getBlock() == targetBlock) count++; + } + } + } + return count; + } + /** countVoxel(pos1, pos2, voxel): number */ + public int countVoxel(GameVector3 pos1, GameVector3 pos2, Object voxel) { + return countVoxel((int) pos1.x, (int) pos1.y, (int) pos1.z, (int) pos2.x, (int) pos2.y, (int) pos2.z, voxel); + } + + /** setVoxelId(x, y, z, voxel: number): number — rotation already encoded in the ID */ + public int setVoxelId(int x, int y, int z, int voxel) { + ServerLevel level = server.overworld(); + BlockPos pos = new BlockPos(x, y, z); + + if (sandbox != null) sandbox.trackBlock(Box3ScriptEngine.get().getCurrentProject(), pos); + + int rot = voxel / ROTATION_MULTIPLIER; + int baseId = voxel % ROTATION_MULTIPLIER; + + if (baseId == 0) { + level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); + return 0; + } + + Block block = resolveBlock(voxel); + if (block == null) return 0; + + BlockState state = applyRotation(block.defaultBlockState(), rot); + level.setBlock(pos, state, 3); + return voxel; + } + /** setVoxelId(pos, voxel): number */ + public int setVoxelId(GameVector3 pos, int voxel) { + return setVoxelId((int) pos.x, (int) pos.y, (int) pos.z, voxel); + } + + // ---- Read ---- + + /** getVoxel(x, y, z): number — base block ID, or 0 for air / unknown. */ + public int getVoxel(int x, int y, int z) { + ServerLevel level = server.overworld(); + BlockState state = level.getBlockState(new BlockPos(x, y, z)); + if (state.isAir()) return 0; + Integer id = blockToId.get(state.getBlock()); + return id != null ? id : 0; + } + /** getVoxel(pos): number */ + public int getVoxel(GameVector3 pos) { + return getVoxel((int) pos.x, (int) pos.y, (int) pos.z); + } + + /** getVoxelId(x, y, z): number — full ID with rotation encoded */ + public int getVoxelId(int x, int y, int z) { + ServerLevel level = server.overworld(); + BlockState state = level.getBlockState(new BlockPos(x, y, z)); + if (state.isAir()) return 0; + Integer boxId = blockToId.get(state.getBlock()); + if (boxId == null) return 0; + int baseId = boxId; + int rot = extractRotation(state); + return rot * ROTATION_MULTIPLIER + baseId; + } + /** getVoxelId(pos): number */ + public int getVoxelId(GameVector3 pos) { + return getVoxelId((int) pos.x, (int) pos.y, (int) pos.z); + } + + /** getVoxelName(x, y, z): string — returns ResourceLocation name of block at position (e.g. "minecraft:stone"). */ + public String getVoxelName(int x, int y, int z) { + ServerLevel level = server.overworld(); + BlockState state = level.getBlockState(new BlockPos(x, y, z)); + if (state.isAir()) return "air"; + Integer boxId = blockToId.get(state.getBlock()); + if (boxId != null) { + String n = idToName.get(boxId); + if (n != null) return n; + } + return BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(); + } + /** getVoxelName(pos): string */ + public String getVoxelName(GameVector3 pos) { + return getVoxelName((int) pos.x, (int) pos.y, (int) pos.z); + } + + /** setSpawner(x, y, z, entityType) */ + public void setSpawner(int x, int y, int z, String entityType) { + ServerLevel level = server.overworld(); + BlockPos pos = new BlockPos(x, y, z); + var be = level.getBlockEntity(pos); + if (!(be instanceof net.minecraft.world.level.block.entity.SpawnerBlockEntity spawnerBe)) return; + + var opt = Box3ScriptUtils.lookupEntityType(entityType); + if (opt == null) return; + + spawnerBe.setEntityId(opt, level.getRandom()); + } + public void setSpawner(GameVector3 pos, String entityType) { + setSpawner((int) pos.x, (int) pos.y, (int) pos.z, entityType); + } + + /** getVoxelRotation(x, y, z): number — 0, 1, 2, 3 */ + public int getVoxelRotation(int x, int y, int z) { + ServerLevel level = server.overworld(); + BlockState state = level.getBlockState(new BlockPos(x, y, z)); + return extractRotation(state); + } + /** getVoxelRotation(pos): number */ + public int getVoxelRotation(GameVector3 pos) { + return getVoxelRotation((int) pos.x, (int) pos.y, (int) pos.z); + } + + /** Resolve Box3 numeric ID from a BlockState. Returns 0 for non-Box3/air blocks. */ + public int getId(BlockState state) { + if (state.isAir()) return 0; + Integer id = blockToId.get(state.getBlock()); + return id != null ? id : 0; + } + + // ---- Internals ---- + + private boolean isAir(Object voxel) { + if (voxel instanceof Number n) return n.intValue() == 0; + if (voxel instanceof String s) return s.equalsIgnoreCase("air"); + return false; + } + + private Block resolveBlock(Object voxel) { + if (voxel instanceof Number n) { + int baseId = n.intValue() % ROTATION_MULTIPLIER; + if (baseId == 0) return null; + // Check Box3 block first + String name = idToName.get(baseId); + if (name != null) { + Block b = resourceToBlock.get(name.toLowerCase(Locale.ROOT)); + if (b != null) return b; + } + return null; + } + if (voxel instanceof String s) { + // Try Box3 block first + Block b = resourceToBlock.get(s.toLowerCase(Locale.ROOT)); + if (b != null) return b; + // Try vanilla block by ResourceLocation (e.g. "minecraft:stone" or "stone") + return Box3ScriptUtils.lookupBlock(s); + } + return null; + } + + private int coerceRotation(Object rotation) { + if (rotation instanceof Number n) { + int r = n.intValue(); + return (r < 0 || r > 3) ? 0 : r; + } + if (rotation instanceof String s) { + try { return Integer.parseInt(s); } catch (NumberFormatException e) { return 0; } + } + return 0; + } + + private BlockState applyRotation(BlockState state, int rot) { + if (rot == 0) return state; + Direction dir = ROTATION_TO_DIRECTION[rot]; + if (state.hasProperty(HorizontalDirectionalBlock.FACING)) { + return state.setValue(HorizontalDirectionalBlock.FACING, dir); + } + for (var prop : state.getProperties()) { + if (prop instanceof DirectionProperty dp && dp.getName().equals("facing")) { + if (dp.getPossibleValues().contains(dir)) { + return state.setValue(dp, dir); + } + } + } + return state; + } + + private int extractRotation(BlockState state) { + if (state.isAir()) return 0; + if (state.hasProperty(HorizontalDirectionalBlock.FACING)) { + return DIRECTION_TO_ROTATION.getOrDefault(state.getValue(HorizontalDirectionalBlock.FACING), 0); + } + for (var prop : state.getProperties()) { + if (prop instanceof DirectionProperty dp && dp.getName().equals("facing")) { + return DIRECTION_TO_ROTATION.getOrDefault(state.getValue(dp), 0); + } + } + return 0; + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSWorld.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSWorld.java new file mode 100644 index 00000000..7f109143 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSWorld.java @@ -0,0 +1,449 @@ +package com.box3lab.box3js.script; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.protocol.game.ClientboundSoundPacket; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.Difficulty; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LightningBolt; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.component.FireworkExplosion; +import net.minecraft.world.item.component.Fireworks; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.storage.ServerLevelData; +import org.mozilla.javascript.Function; + +import java.util.*; + +public class Box3JSWorld { + + private final MinecraftServer server; + private final Box3ScriptEngine engine; + private String projectName; + private final Box3JSScoreboard scoreboard; + private final Box3JSTeam team; + private final Box3JSBossbar bossbar; + private final Box3JSQuery query; + + public Box3JSWorld(MinecraftServer server, Box3ScriptEngine engine) { + this.server = server; + this.engine = engine; + this.scoreboard = new Box3JSScoreboard(server); + this.team = new Box3JSTeam(server); + this.bossbar = new Box3JSBossbar(server); + this.query = new Box3JSQuery(server, engine); + } + + public void setProjectName(String name) { this.projectName = name; } + + /** Clean up all bossbar/scoreboard/team state for one project. */ + public void removeProject(String project) { + bossbar.removeProject(project); + scoreboard.removeProject(project); + team.removeProject(project); + } + + /** Remove ALL bossbar/scoreboard/team state across all projects. */ + public void resetAll() { + bossbar.resetAll(); + scoreboard.resetAll(); + team.resetAll(); + } + + private void trackIfSandboxed() { + engine.getSandbox().trackWorld(engine.getCurrentProject()); + } + + // ---- World properties ---- + + public String projectName() { return server.getMotd(); } + + public int currentTick() { return server.getTickCount(); } + + public double getRainDensity() { return server.overworld().getRainLevel(1.0f); } + public void setRainDensity(double v) { trackIfSandboxed(); server.overworld().getLevelData().setRaining(v > 0); } + + public double getThunderDensity() { return server.overworld().getThunderLevel(1.0f); } + public void setThunderDensity(double v) { + trackIfSandboxed(); + ((ServerLevelData) server.overworld().getLevelData()).setThundering(v > 0); + } + + public void clearWeather() { + trackIfSandboxed(); + var level = server.overworld(); + level.getLevelData().setRaining(false); + ((ServerLevelData) level.getLevelData()).setThundering(false); + } + + // ---- Time ---- + + public long getTime() { return server.overworld().getDayTime(); } + public void setTime(long tick) { trackIfSandboxed(); server.overworld().setDayTime(tick); } + + public double getTimeScale() { + return server.overworld().getGameRules().getBoolean(GameRules.RULE_DAYLIGHT) ? 1.0 : 0.0; + } + public void setTimeScale(double v) { + trackIfSandboxed(); + server.overworld().getGameRules().getRule(GameRules.RULE_DAYLIGHT).set(v > 0, server); + } + + // ---- Difficulty ---- + + public String getDifficulty() { return server.overworld().getDifficulty().getKey(); } + public void setDifficulty(Object v) { + trackIfSandboxed(); + Difficulty diff = v instanceof Number n ? Difficulty.byId(n.intValue()) : Difficulty.byName(v.toString()); + if (diff != null) server.setDifficulty(diff, true); + } + + // ---- Game Rules ---- + + public Object getGameRule(String name) { + GameRules rules = server.overworld().getGameRules(); + return switch (name) { + case "doDaylightCycle" -> rules.getBoolean(GameRules.RULE_DAYLIGHT); + case "doWeatherCycle" -> rules.getBoolean(GameRules.RULE_WEATHER_CYCLE); + case "keepInventory" -> rules.getBoolean(GameRules.RULE_KEEPINVENTORY); + case "doMobSpawning" -> rules.getBoolean(GameRules.RULE_DOMOBSPAWNING); + case "doFireTick" -> rules.getBoolean(GameRules.RULE_DOFIRETICK); + case "mobGriefing" -> rules.getBoolean(GameRules.RULE_MOBGRIEFING); + case "doImmediateRespawn" -> rules.getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN); + default -> null; + }; + } + + public void setGameRule(String name, Object value) { + trackIfSandboxed(); + GameRules rules = server.overworld().getGameRules(); + switch (name) { + case "doDaylightCycle": rules.getRule(GameRules.RULE_DAYLIGHT).set(Box3ScriptUtils.coerceBool(value), server); break; + case "doWeatherCycle": rules.getRule(GameRules.RULE_WEATHER_CYCLE).set(Box3ScriptUtils.coerceBool(value), server); break; + case "keepInventory": rules.getRule(GameRules.RULE_KEEPINVENTORY).set(Box3ScriptUtils.coerceBool(value), server); break; + case "doMobSpawning": rules.getRule(GameRules.RULE_DOMOBSPAWNING).set(Box3ScriptUtils.coerceBool(value), server); break; + case "doFireTick": rules.getRule(GameRules.RULE_DOFIRETICK).set(Box3ScriptUtils.coerceBool(value), server); break; + case "mobGriefing": rules.getRule(GameRules.RULE_MOBGRIEFING).set(Box3ScriptUtils.coerceBool(value), server); break; + case "doImmediateRespawn": rules.getRule(GameRules.RULE_DO_IMMEDIATE_RESPAWN).set(Box3ScriptUtils.coerceBool(value), server); break; + } + } + + // ---- Spawn ---- + + public GameVector3 getSpawnPoint() { + var pos = server.overworld().getSharedSpawnPos(); + return new GameVector3(pos.getX(), pos.getY(), pos.getZ()); + } + public void setWorldSpawn(GameVector3 pos) { + server.overworld().setDefaultSpawnPos(new BlockPos((int) pos.x, (int) pos.y, (int) pos.z), 0); + } + + // ---- Entity spawning ---- + + public Box3JSEntity spawnEntity(String type, GameVector3 pos) { + EntityType eType = Box3ScriptUtils.lookupEntityType(type); + if (eType == null) return null; + Entity entity = eType.create(server.overworld()); + if (entity == null) return null; + entity.setPos(pos.x, pos.y, pos.z); + server.overworld().addFreshEntity(entity); + engine.getSandbox().trackEntity(engine.getCurrentProject(), entity); + return new Box3JSEntity(entity, server, engine); + } + + // ---- Events ---- + + public void onTick(Function handler) { + engine.addTickCallback(() -> engine.callFunction(handler)); + } + public void onPlayerJoin(Function handler) { + engine.addJoinCallback(entity -> engine.callFunction(handler, entity)); + } + public void onPlayerLeave(Function handler) { + engine.addLeaveCallback(entity -> engine.callFunction(handler, entity)); + } + public void onVoxelDestroy(Function handler) { + engine.addVoxelDestroyCallback((entity, x, y, z, voxel, tick) -> + engine.callFunction(handler, entity, x, y, z, voxel, tick)); + } + public void onVoxelContact(Function handler) { + engine.addVoxelContactCallback((entity, voxel, x, y, z, axis, force, tick) -> + engine.callFunction(handler, entity, voxel, x, y, z, axis, force, tick)); + } + public void onInteract(Function handler) { + engine.addInteractCallback((entity, target, tick) -> + engine.callFunction(handler, entity, target, tick)); + } + public void onChat(Function handler) { + engine.addChatCallback((entity, message, tick) -> + engine.callFunction(handler, entity, message, tick)); + } + public void onFluidEnter(Function handler) { + engine.addFluidEnterCallback((entity, fluid, x, y, z, tick) -> + engine.callFunction(handler, entity, fluid, x, y, z, tick)); + } + public void onFluidLeave(Function handler) { + engine.addFluidLeaveCallback((entity, fluid, x, y, z, tick) -> + engine.callFunction(handler, entity, fluid, x, y, z, tick)); + } + public void onEntityContact(Function handler) { + engine.addEntityContactCallback((entity, other, tick) -> + engine.callFunction(handler, entity, other, tick)); + } + public void onEntitySeparate(Function handler) { + engine.addEntitySeparateCallback((entity, other, tick) -> + engine.callFunction(handler, entity, other, tick)); + } + public void onBlockPlace(Function handler) { + engine.addBlockPlaceCallback((entity, x, y, z, voxel, voxelId, tick) -> + engine.callFunction(handler, entity, x, y, z, voxel, voxelId, tick)); + } + public void onEntityDeath(Function handler) { + engine.addEntityDeathCallback((entity, killer, tick) -> + engine.callFunction(handler, entity, killer, tick)); + } + public void onPlayerRespawn(Function handler) { + engine.addRespawnCallback(entity -> engine.callFunction(handler, entity)); + } + public void onBlockActivate(Function handler) { + engine.addBlockActivateCallback((entity, x, y, z, voxel, tick) -> + engine.callFunction(handler, entity, x, y, z, voxel, tick)); + } + public void onEntityDamage(Function handler) { + engine.addEntityDamageCallback((entity, amount, source, attacker, tick) -> + engine.callFunction(handler, entity, amount, source, attacker, tick)); + } + public void onMessage(Function handler) { + String project = engine.getCurrentProject(); + if (project != null) { + engine.addMessageCallback(project, (from, d) -> engine.callFunction(handler, from, d)); + } + } + + // ---- Entity Query ---- + + public List querySelectorAll(String selector) { return query.querySelectorAll(selector); } + public Box3JSEntity querySelector(String selector) { return query.querySelector(selector); } + + // ---- Chat ---- + + public void say(String message) { + server.getPlayerList().broadcastSystemMessage(net.minecraft.network.chat.Component.literal(message), false); + } + + // ---- Timers ---- + + public int setTimeout(Function handler, int ticks) { return engine.scheduleTimeout(handler, ticks); } + public int setInterval(Function handler, int ticks) { return engine.scheduleInterval(handler, ticks); } + public void clearTimeout(int id) { engine.clearTimer(id); } + public void clearInterval(int id) { engine.clearTimer(id); } + + // ---- Command ---- + + public void runCommand(String cmd) { + server.getCommands().performPrefixedCommand(server.createCommandSourceStack(), cmd); + } + + // ---- Scoreboard ---- + + public void addScoreboard(String name) { scoreboard.addScoreboard(engine.getCurrentProject(), name); } + public void addScoreboard(String name, String criteria) { scoreboard.addScoreboard(engine.getCurrentProject(), name, criteria); } + public void removeScoreboard(String name) { scoreboard.removeScoreboard(name); } + public void setScore(Object entityOrName, String objectiveName, int value) { scoreboard.setScore(entityOrName, objectiveName, value); } + public int getScore(Object entityOrName, String objectiveName) { return scoreboard.getScore(entityOrName, objectiveName); } + public void showScoreboard(String slot, String objectiveName) { scoreboard.showScoreboard(slot, objectiveName); } + public void hideScoreboard(String slot) { scoreboard.hideScoreboard(slot); } + public java.util.List listScores(String objectiveName) { return scoreboard.listScores(objectiveName); } + + // ---- Boss Bar ---- + + public void showBossbar(String name, String text, double progress, String colorName) { bossbar.showBossbar(engine.getCurrentProject(), name, text, progress, colorName); } + public void removeBossbar(String name) { bossbar.removeBossbar(engine.getCurrentProject(), name); } + + // ---- Team ---- + + public void createTeam(String name, String colorName) { team.createTeam(engine.getCurrentProject(), name, colorName); } + public void removeTeam(String name) { team.removeTeam(name); } + public void joinTeam(Object entityOrName, String teamName) { team.joinTeam(entityOrName, teamName); } + public void leaveTeam(Object entityOrName) { team.leaveTeam(entityOrName); } + public String getTeamOf(Object entityOrName) { return team.getTeamOf(entityOrName); } + + // ---- World Border ---- + + public double getBorderSize() { return server.overworld().getWorldBorder().getSize(); } + public void setBorderCenter(double x, double z) { trackIfSandboxed(); server.overworld().getWorldBorder().setCenter(x, z); } + public void setBorderSize(double size) { trackIfSandboxed(); server.overworld().getWorldBorder().setSize(size); } + public void shrinkBorder(double targetSize, double seconds) { + trackIfSandboxed(); + WorldBorder border = server.overworld().getWorldBorder(); + border.lerpSizeBetween(border.getSize(), targetSize, (long)(seconds * 1000)); + } + public void setBorderDamage(double damage) { trackIfSandboxed(); server.overworld().getWorldBorder().setDamagePerBlock(damage); } + public void setBorderWarning(int blocks) { trackIfSandboxed(); server.overworld().getWorldBorder().setWarningBlocks(blocks); } + + // ---- Lightning ---- + + public boolean strikeLightning(double x, double y, double z) { + LightningBolt bolt = EntityType.LIGHTNING_BOLT.create(server.overworld()); + if (bolt == null) return false; + bolt.moveTo(x, y, z); + bolt.setVisualOnly(false); + server.overworld().addFreshEntity(bolt); + return true; + } + public boolean strikeLightning(GameVector3 pos) { return strikeLightning(pos.x, pos.y, pos.z); } + public boolean strikeLightning(double x, double y, double z, double damage) { + LightningBolt bolt = EntityType.LIGHTNING_BOLT.create(server.overworld()); + if (bolt == null) return false; + bolt.moveTo(x, y, z); + bolt.setDamage((float) damage); + bolt.setVisualOnly(false); + server.overworld().addFreshEntity(bolt); + return true; + } + public boolean strikeLightning(GameVector3 pos, double damage) { return strikeLightning(pos.x, pos.y, pos.z, damage); } + + // ---- Projectile ---- + + public Box3JSEntity launchProjectile(String type, double x, double y, double z, + double tx, double ty, double tz, double speed) { + EntityType eType = Box3ScriptUtils.lookupEntityType(type); + if (eType == null) return null; + Entity entity = eType.create(server.overworld()); + if (entity == null) return null; + entity.moveTo(x, y, z); + double dx = tx - x, dy = ty - y, dz = tz - z; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (dist > 0.001) { + double s = speed / dist; + entity.setDeltaMovement(dx * s, dy * s, dz * s); + } + if (entity instanceof net.minecraft.world.entity.projectile.Projectile proj) { + proj.shoot(dx, dy, dz, (float) speed, 0); + } + server.overworld().addFreshEntity(entity); + return new Box3JSEntity(entity, server, engine); + } + public Box3JSEntity launchProjectile(String type, GameVector3 pos, GameVector3 target, double speed) { + return launchProjectile(type, pos.x, pos.y, pos.z, target.x, target.y, target.z, speed); + } + + // ---- Firework ---- + + public void launchFirework(double x, double y, double z, String color, String shape) { + int colorInt = switch (color != null ? color.toLowerCase(java.util.Locale.ROOT) : "") { + case "red" -> 0xFF0000; + case "blue" -> 0x0000FF; + case "green", "lime" -> 0x00FF00; + case "yellow" -> 0xFFFF00; + case "gold", "orange" -> 0xFFAA00; + case "white" -> 0xFFFFFF; + case "aqua", "cyan" -> 0x00FFFF; + case "pink", "magenta" -> 0xFF00FF; + case "purple" -> 0xAA00FF; + default -> 0xFFFFFF; + }; + FireworkExplosion.Shape fireworkShape = switch (shape != null ? shape.toLowerCase() : "ball") { + case "large_ball" -> FireworkExplosion.Shape.LARGE_BALL; + case "star" -> FireworkExplosion.Shape.STAR; + case "creeper" -> FireworkExplosion.Shape.CREEPER; + case "burst" -> FireworkExplosion.Shape.BURST; + default -> FireworkExplosion.Shape.SMALL_BALL; + }; + var explosion = new FireworkExplosion(fireworkShape, + new it.unimi.dsi.fastutil.ints.IntArrayList(new int[]{colorInt}), + new it.unimi.dsi.fastutil.ints.IntArrayList(new int[]{colorInt}), + false, true); + var fireworks = new Fireworks(1, java.util.List.of(explosion)); + ItemStack rocket = new ItemStack(Items.FIREWORK_ROCKET); + rocket.set(DataComponents.FIREWORKS, fireworks); + var entity = new net.minecraft.world.entity.projectile.FireworkRocketEntity(server.overworld(), x, y, z, rocket); + server.overworld().addFreshEntity(entity); + } + public void launchFirework(GameVector3 pos, String color, String shape) { + launchFirework(pos.x, pos.y, pos.z, color, shape); + } + + // ---- Particle ---- + + public void spawnParticle(String type, double x, double y, double z, int count, double dx, double dy, double dz, double speed) { + var particle = Box3ScriptUtils.lookupParticle(type); + if (particle != null) server.overworld().sendParticles(particle, x, y, z, count, dx, dy, dz, speed); + } + public void spawnParticle(String type, GameVector3 pos, int count, double dx, double dy, double dz, double speed) { + spawnParticle(type, pos.x, pos.y, pos.z, count, dx, dy, dz, speed); + } + public void spawnParticleCircle(double x, double y, double z, double radius, String type, int count) { + var particle = Box3ScriptUtils.lookupParticle(type); + if (particle == null) return; + for (int i = 0; i < count; i++) { + double angle = (2.0 * Math.PI * i) / count; + server.overworld().sendParticles(particle, x + Math.cos(angle) * radius, y, z + Math.sin(angle) * radius, 1, 0, 0, 0, 0); + } + } + public void spawnParticleCircle(GameVector3 pos, double radius, String type, int count) { + spawnParticleCircle(pos.x, pos.y, pos.z, radius, type, count); + } + + // ---- Drop Item ---- + + public void dropItem(double x, double y, double z, String itemId, int count) { + var item = Box3ScriptUtils.lookupItem(itemId); + if (item == null) return; + ItemStack stack = new ItemStack(item, Math.max(1, count)); + server.overworld().addFreshEntity(new ItemEntity(server.overworld(), x, y, z, stack)); + } + public void dropItem(GameVector3 pos, String itemId, int count) { + dropItem(pos.x, pos.y, pos.z, itemId, count); + } + + // ---- Query ---- + + public Object raycast(GameVector3 origin, GameVector3 direction) { return query.raycast(origin, direction); } + public Object raycast(GameVector3 origin, GameVector3 direction, double maxDistance) { return query.raycast(origin, direction, maxDistance); } + public List entitiesInArea(GameVector3 pos1, GameVector3 pos2) { return query.entitiesInArea(pos1, pos2); } + public List entitiesInRadius(double x, double y, double z, double radius) { return query.entitiesInRadius(x, y, z, radius); } + public List entitiesInRadius(GameVector3 pos, double radius) { return query.entitiesInRadius(pos, radius); } + public String getBiome(int x, int y, int z) { return query.getBiome(x, y, z); } + public String getBiome(GameVector3 pos) { return query.getBiome(pos); } + + // ---- Explode ---- + + public void explode(double x, double y, double z, double power) { explode(x, y, z, power, false); } + public void explode(GameVector3 pos, double power) { explode(pos.x, pos.y, pos.z, power, false); } + public void explode(double x, double y, double z, double power, boolean fire) { + server.overworld().explode(null, x, y, z, (float) power, fire, Level.ExplosionInteraction.BLOCK); + } + public void explode(GameVector3 pos, double power, boolean fire) { explode(pos.x, pos.y, pos.z, power, fire); } + + // ---- Sound ---- + + public void playSound(String path, double x, double y, double z, double volume, double pitch) { + var sound = Box3ScriptUtils.lookupSoundEvent(path); + if (sound == null) return; + var packet = new ClientboundSoundPacket(sound, SoundSource.PLAYERS, x, y, z, (float) volume, (float) pitch, server.overworld().getRandom().nextLong()); + for (var sp : server.getPlayerList().getPlayers()) sp.connection.send(packet); + } + public void playSound(String path, GameVector3 pos, double volume, double pitch) { + playSound(path, pos.x, pos.y, pos.z, volume, pitch); + } + + // ---- Message ---- + + public void sendMessage(String target, Object data) { + engine.fireMessage(engine.getCurrentProject(), target, data); + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptCommand.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptCommand.java new file mode 100644 index 00000000..e726e6a6 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptCommand.java @@ -0,0 +1,296 @@ +package com.box3lab.box3js.script; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; +import net.neoforged.neoforge.event.RegisterCommandsEvent; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +import static net.minecraft.commands.Commands.literal; +import static net.minecraft.commands.Commands.argument; + +public class Box3ScriptCommand { + + private static Box3ScriptWatcher watcher; + + public static void register(RegisterCommandsEvent event) { + event.getDispatcher().register( + literal("box3script") + .requires(src -> src.hasPermission(2)) + .executes(ctx -> listProjects(ctx.getSource())) + .then(createCommand()) + .then(stopCommand()) + .then(onCommand()) + .then(offCommand()) + .then(reloadCommand()) + .then(watchCommand()) + .then(sandboxCommand()) + ); + } + + private static int listProjects(CommandSourceStack source) { + var config = Box3ScriptConfig.get(); + config.discover(source.getServer()); + var projects = config.listProjects(); + var sandbox = Box3ScriptEngine.get().getSandbox(); + if (projects.isEmpty()) { + source.sendSuccess( + () -> Component.literal("No projects found in config/box3/script/"), false); + } else { + StringBuilder sb = new StringBuilder("§6=== Projects ===\n"); + projects.forEach((name, enabled) -> { + String status = enabled ? "§a[ON]" : "§c[OFF]"; + String sbx = sandbox.isEnabled(name) ? " §d[SANDBOX]" : ""; + sb.append(" ").append(status).append(sbx).append(" §f").append(name).append("\n"); + }); + source.sendSuccess( + () -> Component.literal(sb.toString().trim()), false); + } + return 1; + } + + // ---- error reporting helpers ---- + + /** Returns an error reporter that sends messages to the given command source. */ + private static Consumer chatReporter(CommandSourceStack source) { + return msg -> source.sendFailure(Component.literal(msg)); + } + + /** Execute an engine operation with error feedback to the command source. */ + private static int safeRun(CommandSourceStack source, String successMsg, Runnable action) { + try { + action.run(); + source.sendSuccess(() -> Component.literal(successMsg), false); + return 1; + } catch (Exception e) { + String err = e.getMessage(); + if (err == null) err = e.getClass().getSimpleName(); + source.sendFailure(Component.literal(err)); + Box3ScriptEngine.get().reportError(err); + return 0; + } + } + + // --- create --- + + private static LiteralArgumentBuilder createCommand() { + return literal("create") + .then(argument("name", StringArgumentType.word()) + .executes(ctx -> { + String name = StringArgumentType.getString(ctx, "name"); + Path projectDir = scriptDir(ctx.getSource().getServer()).resolve(name).normalize(); + if (Files.exists(projectDir)) { + ctx.getSource().sendFailure(Component.literal("Project already exists: " + name)); + return 0; + } + try { + Box3ScriptTemplate.copyTo(projectDir, name); + Component msg = Component.literal("Project created: " + name + "\n") + .append(clickablePath(projectDir)) + .append(Component.literal(" cd config/box3/script/" + name + + "\n npm install && npm run build" + + "\nUse /box3script on " + name + " to enable it.")); + ctx.getSource().sendSuccess(() -> msg, false); + } catch (Exception e) { + ctx.getSource().sendFailure(Component.literal("Failed to create: " + e.getMessage())); + } + return 1; + })); + } + + // --- stop --- + + private static LiteralArgumentBuilder stopCommand() { + return literal("stop") + .executes(ctx -> safeRun(ctx.getSource(), "All scripts stopped.", () -> + Box3ScriptEngine.get().reset() + )) + .then(argument("project", StringArgumentType.word()) + .suggests(Box3ScriptCommand::suggestProjects) + .executes(ctx -> { + String project = StringArgumentType.getString(ctx, "project"); + Box3ScriptConfig.get().setEnabled(project, false); + return safeRun(ctx.getSource(), "Stopped: " + project, () -> + Box3ScriptEngine.get().removeProject(project) + ); + })); + } + + // --- on --- + + private static LiteralArgumentBuilder onCommand() { + return literal("on") + .then(argument("project", StringArgumentType.word()) + .suggests(Box3ScriptCommand::suggestProjects) + .executes(ctx -> { + String project = StringArgumentType.getString(ctx, "project"); + Box3ScriptConfig.get().setEnabled(project, true); + return safeRun(ctx.getSource(), "Enabled and loaded: " + project, () -> { + Box3ScriptEngine engine = Box3ScriptEngine.get(); + engine.withErrorReporter(chatReporter(ctx.getSource())); + engine.setCurrentProject(project); + try { engine.eval("require('./app')"); } + finally { engine.setCurrentProject(null); engine.clearErrorReporter(); } + }); + })) + .then(literal("all") + .executes(ctx -> { + Box3ScriptConfig.get().setAllEnabled(true); + return safeRun(ctx.getSource(), "All projects enabled.", () -> { + Box3ScriptEngine engine = Box3ScriptEngine.get(); + engine.reset(); + engine.autoLoad(ctx.getSource().getServer()); + }); + })); + } + + // --- off --- + + private static LiteralArgumentBuilder offCommand() { + return literal("off") + .then(argument("project", StringArgumentType.word()) + .suggests(Box3ScriptCommand::suggestProjects) + .executes(ctx -> { + String project = StringArgumentType.getString(ctx, "project"); + Box3ScriptConfig.get().setEnabled(project, false); + Box3ScriptEngine.get().removeProject(project); + ctx.getSource().sendSuccess(() -> Component.literal("Disabled: " + project), false); + return 1; + })) + .then(literal("all") + .executes(ctx -> { + Box3ScriptConfig.get().setAllEnabled(false); + ctx.getSource().sendSuccess(() -> Component.literal("All projects disabled."), false); + return 1; + })); + } + + // --- reload --- + + private static LiteralArgumentBuilder reloadCommand() { + return literal("reload") + .executes(ctx -> safeRun(ctx.getSource(), "Scripts reloaded.", () -> { + Box3ScriptEngine engine = Box3ScriptEngine.get(); + engine.withErrorReporter(chatReporter(ctx.getSource())); + try { + engine.reset(); + engine.autoLoad(ctx.getSource().getServer()); + } finally { + engine.clearErrorReporter(); + } + })) + .then(argument("project", StringArgumentType.word()) + .suggests(Box3ScriptCommand::suggestProjects) + .executes(ctx -> { + String project = StringArgumentType.getString(ctx, "project"); + Box3ScriptConfig.get().setEnabled(project, true); + return safeRun(ctx.getSource(), "Reloaded: " + project, () -> { + Box3ScriptEngine engine = Box3ScriptEngine.get(); + engine.withErrorReporter(chatReporter(ctx.getSource())); + engine.setCurrentProject(project); + try { + engine.removeProject(project); + engine.eval("require('./app')"); + } finally { + engine.setCurrentProject(null); + engine.clearErrorReporter(); + } + }); + })); + } + + // --- watch --- + + private static LiteralArgumentBuilder watchCommand() { + return literal("watch") + .executes(ctx -> { + if (watcher == null) watcher = new Box3ScriptWatcher(ctx.getSource().getServer()); + if (watcher.isRunning()) { + watcher.stop(); + ctx.getSource().sendSuccess(() -> Component.literal("File watching stopped."), false); + } else { + watcher.start(); + ctx.getSource().sendSuccess(() -> Component.literal("File watching started. Changes will auto-reload."), false); + } + return 1; + }) + .then(literal("on") + .executes(ctx -> { + if (watcher == null) watcher = new Box3ScriptWatcher(ctx.getSource().getServer()); + if (watcher.isRunning()) { + ctx.getSource().sendSuccess(() -> Component.literal("Already watching."), false); + } else { + watcher.start(); + ctx.getSource().sendSuccess(() -> Component.literal("File watching started."), false); + } + return 1; + })) + .then(literal("off") + .executes(ctx -> { + if (watcher != null && watcher.isRunning()) { + watcher.stop(); + ctx.getSource().sendSuccess(() -> Component.literal("File watching stopped."), false); + } else { + ctx.getSource().sendSuccess(() -> Component.literal("Not watching."), false); + } + return 1; + })); + } + + // --- sandbox --- + + private static LiteralArgumentBuilder sandboxCommand() { + return literal("sandbox") + .then(argument("project", StringArgumentType.word()) + .suggests(Box3ScriptCommand::suggestProjects) + .executes(ctx -> { + String project = StringArgumentType.getString(ctx, "project"); + var sb = Box3ScriptEngine.get().getSandbox(); + if (sb.isEnabled(project)) { + var summary = sb.disable(project); + String detail = summary.hasAny() ? " — restored: " + summary.toMessage() : ""; + ctx.getSource().sendSuccess(() -> Component.literal("Sandbox OFF for " + project + detail), false); + } else { + sb.enable(project); + ctx.getSource().sendSuccess(() -> Component.literal("Sandbox ON for " + project + " — all changes tracked for rollback."), false); + } + return 1; + })); + } + + // --- helpers --- + + private static CompletableFuture suggestProjects(CommandContext ctx, SuggestionsBuilder builder) { + var config = Box3ScriptConfig.get(); + config.discover(ctx.getSource().getServer()); + for (String name : config.listProjects().keySet()) { + if (builder.getRemaining().isEmpty() || name.startsWith(builder.getRemaining())) { + builder.suggest(name); + } + } + return builder.buildFuture(); + } + + private static Component clickablePath(Path path) { + String absPath = path.toAbsolutePath().toString(); + return Component.literal(" §b§n[Copy path]§r\n") + .withStyle(style -> style + .withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, absPath)) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.literal("Click to copy path")))); + } + + private static Path scriptDir(net.minecraft.server.MinecraftServer server) { + return Box3ScriptConfig.get().getScriptDir(server); + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptConfig.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptConfig.java new file mode 100644 index 00000000..b7109b82 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptConfig.java @@ -0,0 +1,89 @@ +package com.box3lab.box3js.script; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import net.minecraft.server.MinecraftServer; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; + +public class Box3ScriptConfig { + + private static final Gson GSON = new Gson(); + private static final Type MAP_TYPE = new TypeToken>() {}.getType(); + + private static Box3ScriptConfig INSTANCE; + + private Path configFile; + private Map projects = new LinkedHashMap<>(); + + public static Box3ScriptConfig get() { + if (INSTANCE == null) INSTANCE = new Box3ScriptConfig(); + return INSTANCE; + } + + private Box3ScriptConfig() {} + + /** Load config from disk. Call once when server starts. */ + public void load(MinecraftServer server) { + configFile = server.getServerDirectory().resolve("config/box3/scripts.json"); + if (Files.exists(configFile)) { + try { + String json = Files.readString(configFile); + Map loaded = GSON.fromJson(json, MAP_TYPE); + if (loaded != null) projects = new LinkedHashMap<>(loaded); + } catch (IOException ignored) {} + } + } + + /** Save config to disk. */ + private void save() { + if (configFile == null) return; + try { + Files.createDirectories(configFile.getParent()); + Files.writeString(configFile, GSON.toJson(projects)); + } catch (IOException ignored) {} + } + + public boolean isEnabled(String projectName) { + return Boolean.TRUE.equals(projects.get(projectName)); + } + + public void setEnabled(String projectName, boolean enabled) { + projects.put(projectName, enabled); + save(); + } + + public void setAllEnabled(boolean enabled) { + projects.replaceAll((k, v) -> enabled); + save(); + } + + public Map listProjects() { + return new LinkedHashMap<>(projects); + } + + /** Scan script directory for new projects, add them as disabled by default. */ + public void discover(MinecraftServer server) { + Path scriptDir = getScriptDir(server); + if (!Files.exists(scriptDir)) return; + try (var dirs = Files.list(scriptDir)) { + dirs.filter(Files::isDirectory).forEach(dir -> { + Path appJs = dir.resolve("app.js"); + if (Files.exists(appJs)) { + String name = dir.getFileName().toString(); + projects.putIfAbsent(name, false); + } + }); + } catch (IOException ignored) {} + save(); + } + + public Path getScriptDir(MinecraftServer server) { + return server.getServerDirectory().resolve("config/box3/script"); + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java new file mode 100644 index 00000000..e2121d51 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java @@ -0,0 +1,714 @@ +package com.box3lab.box3js.script; + +import com.box3lab.box3js.Box3JS; +import net.minecraft.core.BlockPos; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.state.BlockState; +import org.mozilla.javascript.*; +import org.mozilla.javascript.commonjs.module.ModuleScriptProvider; +import org.mozilla.javascript.commonjs.module.Require; +import org.mozilla.javascript.commonjs.module.RequireBuilder; +import org.mozilla.javascript.commonjs.module.provider.StrongCachingModuleScriptProvider; +import org.mozilla.javascript.commonjs.module.provider.UrlModuleSourceProvider; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +public class Box3ScriptEngine { + + private static final Box3ScriptEngine INSTANCE = new Box3ScriptEngine(); + + private ScriptableObject scope; + private Box3JSWorld worldBinding; + private Box3JSVoxels voxelsBinding; + private Box3JSStorage storageBinding; + private Box3ScriptSandbox sandbox; + private MinecraftServer server; + private boolean initialized; + + final Box3JSEventBus bus = new Box3JSEventBus(); + private String currentProject; + private Consumer errorReporter; + private final Map projectRequires = new HashMap<>(); + + public static Box3ScriptEngine get() { + return INSTANCE; + } + + public void init(MinecraftServer server) { + if (initialized) return; + this.server = server; + this.sandbox = new Box3ScriptSandbox(server.overworld()); + this.worldBinding = new Box3JSWorld(server, this); + this.voxelsBinding = new Box3JSVoxels(server, sandbox); + this.storageBinding = new Box3JSStorage(server.getServerDirectory().resolve("config"), this); + setupScope(); + initialized = true; + } + + /** Execute app.js for enabled projects under config/box3/script/ */ + public void autoLoad(MinecraftServer server) { + init(server); + Box3ScriptConfig config = Box3ScriptConfig.get(); + config.load(server); + config.discover(server); + + Path scriptDir = config.getScriptDir(server); + if (!Files.exists(scriptDir)) return; + try (var dirs = Files.list(scriptDir)) { + dirs.filter(Files::isDirectory) + .sorted() + .forEach(project -> { + String name = project.getFileName().toString(); + Path appJs = project.resolve("dist/app.js"); + if (!Files.exists(appJs)) { + appJs = project.resolve("app.js"); + } + if (Files.exists(appJs) && config.isEnabled(name)) { + try { + setCurrentProject(name); + eval("require('./app')"); + Box3JS.LOGGER.info("Auto-loaded project: {}", name); + } catch (Exception e) { + Box3JS.LOGGER.error("Failed to auto-load: {}", appJs, e); + } finally { + setCurrentProject(null); + } + } + }); + } catch (IOException ignored) {} + } + + public Object eval(String code) { + if (!initialized) throw new IllegalStateException("ScriptEngine not initialized"); + Context cx = Context.enter(); + try { + return cx.evaluateString(scope, code, "script", 1, null); + } finally { + Context.exit(); + } + } + + /** Report error to the current errorReporter (player), or just log if none. */ + void reportError(String msg) { + Box3JS.LOGGER.error(msg); + if (errorReporter != null) errorReporter.accept(msg); + } + + /** Set reporter for the current operation, clear after. Returns self for chaining. */ + Box3ScriptEngine withErrorReporter(Consumer reporter) { + this.errorReporter = reporter; + return this; + } + void clearErrorReporter() { this.errorReporter = null; } + + // ---- Callback registration (all wrap project context) ---- + + public void addTickCallback(Runnable cb) { + String project = currentProject; + bus.addTick(project, wrapContext(project, cb)); + } + public void addJoinCallback(PlayerJoinCallback cb) { + String project = currentProject; + bus.addJoin(project, (e) -> runInContext(project, () -> cb.onJoin(e))); + } + public void addLeaveCallback(PlayerLeaveCallback cb) { + String project = currentProject; + bus.addLeave(project, (e) -> runInContext(project, () -> cb.onLeave(e))); + } + public void addVoxelDestroyCallback(VoxelDestroyCallback cb) { + String project = currentProject; + bus.addVoxelDestroy(project, (e, x, y, z, v, t) -> runInContext(project, () -> cb.onDestroy(e, x, y, z, v, t))); + } + public void addVoxelContactCallback(VoxelContactCallback cb) { + String project = currentProject; + bus.addVoxelContact(project, (e, v, x, y, z, a, f, t) -> runInContext(project, () -> cb.onContact(e, v, x, y, z, a, f, t))); + } + public void addInteractCallback(InteractCallback cb) { + String project = currentProject; + bus.addInteract(project, (e, tgt, tick) -> runInContext(project, () -> cb.onInteract(e, tgt, tick))); + } + public void addChatCallback(ChatCallback cb) { + String project = currentProject; + bus.addChat(project, (e, msg, tick) -> runInContext(project, () -> cb.onChat(e, msg, tick))); + } + public void addFluidEnterCallback(FluidEnterCallback cb) { + String project = currentProject; + bus.addFluidEnter(project, (e, f, x, y, z, t) -> runInContext(project, () -> cb.onEnter(e, f, x, y, z, t))); + } + public void addFluidLeaveCallback(FluidLeaveCallback cb) { + String project = currentProject; + bus.addFluidLeave(project, (e, f, x, y, z, t) -> runInContext(project, () -> cb.onLeave(e, f, x, y, z, t))); + } + public void addEntityContactCallback(EntityContactCallback cb) { + String project = currentProject; + bus.addEntityContact(project, (e, o, t) -> runInContext(project, () -> cb.onContact(e, o, t))); + } + public void addEntitySeparateCallback(EntitySeparateCallback cb) { + String project = currentProject; + bus.addEntitySeparate(project, (e, o, t) -> runInContext(project, () -> cb.onSeparate(e, o, t))); + } + public void addBlockPlaceCallback(BlockPlaceCallback cb) { + String project = currentProject; + bus.addBlockPlace(project, (e, x, y, z, v, vid, t) -> runInContext(project, () -> cb.onPlace(e, x, y, z, v, vid, t))); + } + public void addEntityDeathCallback(EntityDeathCallback cb) { + String project = currentProject; + bus.addEntityDeath(project, (e, k, t) -> runInContext(project, () -> cb.onDeath(e, k, t))); + } + public void addRespawnCallback(PlayerRespawnCallback cb) { + String project = currentProject; + bus.addRespawn(project, (e) -> runInContext(project, () -> cb.onRespawn(e))); + } + public void addBlockActivateCallback(BlockActivateCallback cb) { + String project = currentProject; + bus.addBlockActivate(project, (e, x, y, z, v, t) -> runInContext(project, () -> cb.onActivate(e, x, y, z, v, t))); + } + public void addEntityDamageCallback(EntityDamageCallback cb) { + String project = currentProject; + bus.addEntityDamage(project, (e, a, s, at, t) -> runInContext(project, () -> cb.onDamage(e, a, s, at, t))); + } + public void addMessageCallback(String project, MessageCallback cb) { + bus.addMessage(project, (from, d) -> runInContext(project, () -> cb.onMessage(from, d))); + } + public void setPlayerChatHandler(UUID uuid, Function handler) { + String project = currentProject; + bus.chatHandlersFor(project).put(uuid, handler); + } + + private Runnable wrapContext(String project, Runnable cb) { + return () -> { + String prev = currentProject; + setCurrentProject(project); + try { cb.run(); } finally { setCurrentProject(prev); } + }; + } + + private void runInContext(String project, Runnable action) { + String prev = currentProject; + setCurrentProject(project); + try { action.run(); } finally { setCurrentProject(prev); } + } + + public void setCurrentProject(String name) { + currentProject = name; + worldBinding.setProjectName(name); + } + public String getCurrentProject() { return currentProject; } + + Box3ScriptSandbox getSandbox() { return sandbox; } + + // ---- Project lifecycle ---- + + /** Remove one project's callbacks, state, and resources without affecting others. */ + public void removeProject(String project) { + bus.removeProject(project); + projectRequires.remove(project); + worldBinding.removeProject(project); + var summary = sandbox.restoreProject(project); + if (summary.hasAny()) { + Box3JS.LOGGER.info("Sandbox [{}] restored: {}", project, summary.toMessage()); + } + Box3JS.LOGGER.info("Removed project: {}", project); + } + + // ---- Message routing ---- + + public void fireMessage(String sender, String target, Object data) { + if ("*".equals(target)) { + for (var entry : bus.messageCallbacks.entrySet()) { + if (!entry.getKey().equals(sender)) { + for (var cb : entry.getValue()) cb.onMessage(sender, data); + } + } + } else { + List cbs = bus.messageCallbacks.get(target); + if (cbs != null) { + for (var cb : cbs) cb.onMessage(sender, data); + } + } + } + + // ---- Timers ---- + + public int scheduleTimeout(Function handler, int ticks) { + String project = currentProject; + int id = bus.nextTimerId(project); + bus.timersFor(project).add(new TimerEntry(id, handler, ticks, 0, project)); + return id; + } + + public int scheduleInterval(Function handler, int ticks) { + String project = currentProject; + int id = bus.nextTimerId(project); + bus.timersFor(project).add(new TimerEntry(id, handler, ticks, ticks, project)); + return id; + } + + public void clearTimer(int id) { + for (var list : bus.timers.values()) { + if (list.removeIf(t -> t.id == id)) return; + } + } + + private void fireTimers() { + for (var list : bus.timers.values()) { + var toFire = new ArrayList(); + var toRemove = new ArrayList(); + for (var t : list) { + if (--t.remaining <= 0) { + toFire.add(t); + if (t.interval == 0) toRemove.add(t); + else t.remaining = t.interval; + } + } + list.removeAll(toRemove); + for (var t : toFire) { + runInContext(t.project, () -> callFunction(t.handler)); + } + } + } + + // ---- Tick ---- + + public void fireTick() { + fireTimers(); + for (var list : bus.tickCallbacks.values()) { + for (Runnable cb : list) cb.run(); + } + // Voxel contact tracking — per-project + for (var entry : bus.voxelContactCallbacks.entrySet()) { + String project = entry.getKey(); + var callbacks = entry.getValue(); + if (callbacks.isEmpty()) continue; + long tick = server.getTickCount(); + var tracked = bus.voxelContactFor(project); + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + UUID uuid = player.getUUID(); + BlockPos current = player.blockPosition(); + BlockPos last = tracked.put(uuid, current); + if (!current.equals(last)) { + Box3JSEntity entity = new Box3JSEntity(player, server, this); + var state = player.level().getBlockState(current); + int voxelId = voxelsBinding.getId(state); + double force = player.getDeltaMovement().length(); + runInContext(project, () -> { + for (var cb : callbacks) { + cb.onContact(entity, voxelId, current.getX(), current.getY(), current.getZ(), 1, force, tick); + } + }); + } + } + } + // Fluid state tracking — per-project + for (var entry : bus.fluidEnterCallbacks.entrySet()) { + tickFluid(entry.getKey(), entry.getValue(), bus.fluidLeaveCallbacks.get(entry.getKey())); + } + for (var entry : bus.fluidLeaveCallbacks.entrySet()) { + if (bus.fluidEnterCallbacks.containsKey(entry.getKey())) continue; // handled above + tickFluid(entry.getKey(), bus.fluidEnterCallbacks.get(entry.getKey()), entry.getValue()); + } + // Entity contact tracking — per-project + for (var entry : bus.entityContactCallbacks.entrySet()) { + String project = entry.getKey(); + var callbacks = entry.getValue(); + if (callbacks.isEmpty()) continue; + long tick = server.getTickCount(); + var pairs = bus.contactPairsFor(project); + var separate = bus.entitySeparateCallbacks.getOrDefault(project, Collections.emptyList()); + var players = server.getPlayerList().getPlayers(); + for (int i = 0; i < players.size(); i++) { + for (int j = i + 1; j < players.size(); j++) { + ServerPlayer a = players.get(i); + ServerPlayer b = players.get(j); + double dist = a.distanceToSqr(b); + String pairKey = a.getStringUUID() + "|" + b.getStringUUID(); + if (dist < 2.25) { + if (pairs.add(pairKey)) { + Box3JSEntity ea = new Box3JSEntity(a, server, this); + Box3JSEntity eb = new Box3JSEntity(b, server, this); + runInContext(project, () -> { + for (var cb : callbacks) cb.onContact(ea, eb, tick); + }); + } + } else if (pairs.remove(pairKey) && !separate.isEmpty()) { + Box3JSEntity ea = new Box3JSEntity(a, server, this); + Box3JSEntity eb = new Box3JSEntity(b, server, this); + runInContext(project, () -> { + for (var cb : separate) cb.onSeparate(ea, eb, tick); + }); + } + } + } + } + } + + private void tickFluid(String project, List enter, List leave) { + if ((enter == null || enter.isEmpty()) && (leave == null || leave.isEmpty())) return; + long tick = server.getTickCount(); + var tracked = bus.fluidStateFor(project); + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + UUID uuid = player.getUUID(); + String current = player.isInLava() ? "lava" : player.isInWater() ? "water" : "none"; + String last = tracked.put(uuid, current); + if (current.equals(last)) continue; + Box3JSEntity entity = new Box3JSEntity(player, server, this); + BlockPos pos = player.blockPosition(); + if (!"none".equals(current) && !"none".equals(last) && last != null) { + runInContext(project, () -> { + if (leave != null) for (var cb : leave) cb.onLeave(entity, last, pos.getX(), pos.getY(), pos.getZ(), tick); + if (enter != null) for (var cb : enter) cb.onEnter(entity, current, pos.getX(), pos.getY(), pos.getZ(), tick); + }); + } else if (!"none".equals(current) && ("none".equals(last) || last == null)) { + runInContext(project, () -> { + if (enter != null) for (var cb : enter) cb.onEnter(entity, current, pos.getX(), pos.getY(), pos.getZ(), tick); + }); + } else if ("none".equals(current) && last != null && !"none".equals(last)) { + runInContext(project, () -> { + if (leave != null) for (var cb : leave) cb.onLeave(entity, last, pos.getX(), pos.getY(), pos.getZ(), tick); + }); + } + } + } + + // ---- Fire methods (iterate all projects) ---- + + private String getBlockIdString(BlockPos pos) { + var state = server.overworld().getBlockState(pos); + var key = state.getBlock().builtInRegistryHolder().key(); + return key != null ? key.location().toString() : "minecraft:air"; + } + + public void fireVoxelDestroy(ServerPlayer player, BlockPos pos) { + String voxel = null; + long tick = -1; + Box3JSEntity entity = null; + for (var entry : bus.voxelDestroyCallbacks.entrySet()) { + if (entry.getValue().isEmpty()) continue; + if (entity == null) { entity = new Box3JSEntity(player, server, this); tick = server.getTickCount(); voxel = getBlockIdString(pos); } + Box3JSEntity e = entity; + long t = tick; + String v = voxel; + runInContext(entry.getKey(), () -> { + for (var cb : entry.getValue()) cb.onDestroy(e, pos.getX(), pos.getY(), pos.getZ(), v, t); + }); + } + } + + public void fireInteract(ServerPlayer player, net.minecraft.world.entity.Entity target) { + Box3JSEntity entity = null; + Box3JSEntity targetEntity = null; + long tick = -1; + for (var entry : bus.interactCallbacks.entrySet()) { + if (entry.getValue().isEmpty()) continue; + if (entity == null) { entity = new Box3JSEntity(player, server, this); targetEntity = new Box3JSEntity(target, server, this); tick = server.getTickCount(); } + Box3JSEntity e = entity; + Box3JSEntity te = targetEntity; + long t = tick; + runInContext(entry.getKey(), () -> { + for (var cb : entry.getValue()) cb.onInteract(e, te, t); + }); + } + } + + public void fireChat(ServerPlayer player, String message) { + Box3JSEntity entity = null; + long tick = -1; + for (var entry : bus.chatCallbacks.entrySet()) { + if (entity == null) { entity = new Box3JSEntity(player, server, this); tick = server.getTickCount(); } + Box3JSEntity e = entity; + long t = tick; + runInContext(entry.getKey(), () -> { + for (var cb : entry.getValue()) cb.onChat(e, message, t); + }); + } + // Per-player chat handlers + for (var entry : bus.playerChatHandlers.entrySet()) { + Function handler = entry.getValue().get(player.getUUID()); + if (handler != null) { + Box3JSEntity e = entity != null ? entity : new Box3JSEntity(player, server, this); + long t = tick != -1 ? tick : server.getTickCount(); + String project = entry.getKey(); + runInContext(project, () -> callFunction(handler, e, message, t)); + } + } + } + + public void fireBlockPlace(ServerPlayer player, BlockPos pos, BlockState state) { + Box3JSEntity entity = null; + long tick = -1; + int voxelId = -1; + String voxel = null; + for (var entry : bus.blockPlaceCallbacks.entrySet()) { + if (entry.getValue().isEmpty()) continue; + if (entity == null) { entity = new Box3JSEntity(player, server, this); tick = server.getTickCount(); voxelId = voxelsBinding.getId(state); voxel = state.isAir() ? "minecraft:air" : state.getBlock().builtInRegistryHolder().key().location().toString(); } + Box3JSEntity e = entity; + long t = tick; + int vid = voxelId; + String v = voxel; + runInContext(entry.getKey(), () -> { + for (var cb : entry.getValue()) cb.onPlace(e, pos.getX(), pos.getY(), pos.getZ(), v, vid, t); + }); + } + } + + public void fireEntityDeath(net.minecraft.world.entity.Entity deadEntity, net.minecraft.world.entity.Entity attacker) { + Box3JSEntity entity = null; + Box3JSEntity killer = null; + long tick = -1; + for (var entry : bus.entityDeathCallbacks.entrySet()) { + if (entry.getValue().isEmpty()) continue; + if (entity == null) { entity = new Box3JSEntity(deadEntity, server, this); killer = attacker != null ? new Box3JSEntity(attacker, server, this) : null; tick = server.getTickCount(); } + Box3JSEntity e = entity; + Box3JSEntity k = killer; + long t = tick; + runInContext(entry.getKey(), () -> { + for (var cb : entry.getValue()) cb.onDeath(e, k, t); + }); + } + } + + public void firePlayerRespawn(ServerPlayer player) { + Box3JSEntity entity = null; + for (var entry : bus.respawnCallbacks.entrySet()) { + if (entry.getValue().isEmpty()) continue; + if (entity == null) entity = new Box3JSEntity(player, server, this); + Box3JSEntity e = entity; + runInContext(entry.getKey(), () -> { + for (var cb : entry.getValue()) cb.onRespawn(e); + }); + } + } + + public void fireBlockActivate(ServerPlayer player, BlockPos pos, BlockState state) { + Box3JSEntity entity = null; + long tick = -1; + String voxel = null; + for (var entry : bus.blockActivateCallbacks.entrySet()) { + if (entry.getValue().isEmpty()) continue; + if (entity == null) { entity = new Box3JSEntity(player, server, this); tick = server.getTickCount(); voxel = state.isAir() ? "minecraft:air" : state.getBlock().builtInRegistryHolder().key().location().toString(); } + Box3JSEntity e = entity; + long t = tick; + String v = voxel; + runInContext(entry.getKey(), () -> { + for (var cb : entry.getValue()) cb.onActivate(e, pos.getX(), pos.getY(), pos.getZ(), v, t); + }); + } + } + + public void fireEntityDamage(net.minecraft.world.entity.Entity damagedEntity, double amount, String source, net.minecraft.world.entity.Entity attacker) { + Box3JSEntity entity = null; + Box3JSEntity attackerEntity = null; + long tick = -1; + for (var entry : bus.entityDamageCallbacks.entrySet()) { + if (entry.getValue().isEmpty()) continue; + if (entity == null) { entity = new Box3JSEntity(damagedEntity, server, this); attackerEntity = attacker != null ? new Box3JSEntity(attacker, server, this) : null; tick = server.getTickCount(); } + Box3JSEntity e = entity; + Box3JSEntity ae = attackerEntity; + long t = tick; + runInContext(entry.getKey(), () -> { + for (var cb : entry.getValue()) cb.onDamage(e, amount, source, ae, t); + }); + } + } + + public void firePlayerJoin(ServerPlayer player) { + Box3JSEntity entity = null; + for (var entry : bus.joinCallbacks.entrySet()) { + if (entry.getValue().isEmpty()) continue; + if (entity == null) entity = new Box3JSEntity(player, server, this); + Box3JSEntity e = entity; + runInContext(entry.getKey(), () -> { + for (var cb : entry.getValue()) cb.onJoin(e); + }); + } + } + + public void firePlayerLeave(ServerPlayer player) { + Box3JSEntity entity = null; + for (var entry : bus.leaveCallbacks.entrySet()) { + if (entry.getValue().isEmpty()) continue; + if (entity == null) entity = new Box3JSEntity(player, server, this); + Box3JSEntity e = entity; + runInContext(entry.getKey(), () -> { + for (var cb : entry.getValue()) cb.onLeave(e); + }); + } + } + + /** Call a JS function from Java, managing Rhino context */ + public Object callFunction(Function fn, Object... args) { + Context cx = Context.enter(); + try { + return fn.call(cx, scope, scope, args); + } finally { + Context.exit(); + } + } + + /** Wrap a Java object for return to JS */ + public Object wrap(Object obj) { + return Context.javaToJS(obj, scope); + } + + public ScriptableObject getScope() { return scope; } + + public Map getCustomProps(UUID uuid) { + return bus.entityCustomProps.computeIfAbsent(uuid, k -> new HashMap<>()); + } + + public void clearCustomProps(UUID uuid) { + bus.entityCustomProps.remove(uuid); + } + + /** Clear all callbacks, state, and reset the JS scope (keeps server binding) */ + public void reset() { + bus.clearAll(); + projectRequires.clear(); + worldBinding.resetAll(); + sandbox.restoreAll(); + var oldSandbox = this.sandbox; + this.sandbox = new Box3ScriptSandbox(server.overworld()); + this.sandbox.inheritEnabled(oldSandbox); + this.worldBinding = new Box3JSWorld(server, this); + this.voxelsBinding = new Box3JSVoxels(server, sandbox); + this.storageBinding = new Box3JSStorage(server.getServerDirectory().resolve("config"), this); + setupScope(); + } + + private void setupScope() { + Context cx = Context.enter(); + try { + scope = cx.initStandardObjects(); + ScriptableObject.putProperty(scope, "world", Context.javaToJS(worldBinding, scope)); + ScriptableObject.putProperty(scope, "voxels", Context.javaToJS(voxelsBinding, scope)); + ScriptableObject.putProperty(scope, "storage", Context.javaToJS(storageBinding, scope)); + ScriptableObject.putProperty(scope, "_jConsole", Context.javaToJS(new Box3JSConsole(), scope)); + cx.evaluateString(scope, + "console = {" + + " log: function() { return _jConsole.log.apply(_jConsole, arguments); }," + + " debug: function() { return _jConsole.debug.apply(_jConsole, arguments); }," + + " warn: function() { return _jConsole.warn.apply(_jConsole, arguments); }," + + " error: function() { return _jConsole.error.apply(_jConsole, arguments); }," + + " clear: function() { return _jConsole.clear.apply(_jConsole, arguments); }," + + " assert: function(a) {" + + " if (!a) {" + + " var b = [];" + + " for (var i = 1; i < arguments.length; i++) b.push(arguments[i]);" + + " _jConsole.error(b.length ? b : ['Assertion failed']);" + + " }" + + " }" + + "};", + "console-init", 1, null); + ScriptableObject.putProperty(scope, "require", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + String moduleId = args[0].toString(); + String project = currentProject; + if (project == null) { + throw ScriptRuntime.throwError(cx, scope, "require() called outside a project context"); + } + Path projectDir = Box3ScriptConfig.get().getScriptDir(server).resolve(project); + Require req = projectRequires.computeIfAbsent(project, p -> { + try { + ModuleScriptProvider provider = new StrongCachingModuleScriptProvider( + new UrlModuleSourceProvider( + Collections.unmodifiableList(java.util.Arrays.asList( + projectDir.resolve("dist").toUri(), + projectDir.toUri())), null) { + @Override + protected String getCharacterEncoding(java.net.URLConnection c) { + return "utf-8"; + } + }); + return new RequireBuilder() + .setModuleScriptProvider(provider) + .setSandboxed(false) + .createRequire(cx, Box3ScriptEngine.this.scope); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + return req.requireMain(cx, moduleId); + } + }); + ScriptableObject.putProperty(scope, "sleep", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + int ms = ((Number) args[0]).intValue(); + try { Thread.sleep(ms); } catch (InterruptedException ignored) {} + return Undefined.instance; + } + }); + ScriptableObject.putProperty(scope, "GameVector3", new NativeJavaClass(scope, GameVector3.class)); + ScriptableObject.putProperty(scope, "GameBounds3", new NativeJavaClass(scope, GameBounds3.class)); + ScriptableObject.putProperty(scope, "GameRGBColor", new NativeJavaClass(scope, GameRGBColor.class)); + ScriptableObject.putProperty(scope, "GameRGBAColor", new NativeJavaClass(scope, GameRGBAColor.class)); + ScriptableObject.putProperty(scope, "GameQuaternion", new NativeJavaClass(scope, GameQuaternion.class)); + cx.evaluateString(scope, + "GameDialogType = { TEXT: 'TEXT', INPUT: 'INPUT', SELECT: 'SELECT' }; " + + "GameButtonType = { WALK: 'WALK', RUN: 'RUN', CROUCH: 'CROUCH', JUMP: 'JUMP', " + + " DOUBLE_JUMP: 'DOUBLE_JUMP', FLY: 'FLY', ACTION0: 'ACTION0', ACTION1: 'ACTION1' }; " + + "GameInputDirection = { NONE: 0, VERTICAL: 1, HORIZONTAL: 2, BOTH: 3 }; " + + "GameCameraMode = { FIXED: 'FIXED', FOLLOW: 'FOLLOW', FPS: 'FPS', RELATIVE: 'RELATIVE' }; " + + "GamePlayerMoveState = { FLYING: 'FLYING', GROUND: 'GROUND', SWIM: 'SWIM', FALL: 'FALL', " + + " JUMP: 'JUMP', DOUBLE_JUMP: 'DOUBLE_JUMP' }; " + + "GamePlayerWalkState = { NONE: 'NONE', CROUCH: 'CROUCH', WALK: 'WALK', RUN: 'RUN' };", + "enums", 1, null); + } finally { + Context.exit(); + } + } + + public MinecraftServer getServer() { return server; } + public Box3JSWorld getWorldBinding() { return worldBinding; } + public Box3JSVoxels getVoxelsBinding() { return voxelsBinding; } + + public class Box3JSConsole { + private void print(String level, Object... args) { + StringBuilder sb = new StringBuilder(); + String proj = currentProject; + if (proj != null) sb.append('[').append(proj).append("] "); + for (Object a : args) sb.append(a).append(' '); + System.out.println("[Box3JS]" + level + " " + sb.toString().trim()); + } + + public void log(Object... args) { print("", args); } + public void debug(Object... args) { print("[DEBUG]", args); } + public void warn(Object... args) { print("[WARN]", args); } + + public void error(Object... args) { + StringBuilder sb = new StringBuilder(); + String proj = currentProject; + if (proj != null) sb.append('[').append(proj).append("] "); + for (Object a : args) sb.append(a).append(' '); + System.err.println("[Box3JS][ERROR] " + sb.toString().trim()); + } + + public void clear() { + System.out.print("\033[H\033[2J"); + System.out.flush(); + } + } + + static class TimerEntry { + final int id; + final Function handler; + int remaining; + final int interval; + final String project; + + TimerEntry(int id, Function handler, int remaining, int interval, String project) { + this.id = id; + this.handler = handler; + this.remaining = remaining; + this.interval = interval; + this.project = project; + } + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptSandbox.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptSandbox.java new file mode 100644 index 00000000..2ac3cd80 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptSandbox.java @@ -0,0 +1,457 @@ +package com.box3lab.box3js.script; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ServerLevelData; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +class Box3ScriptSandbox { + + private static final int MAX_BLOCK_CHANGES = 5_000_000; + private static final double WARN_THRESHOLD = 0.9; + + private final Map> blockChanges = new ConcurrentHashMap<>(); + private final Map> spawnedEntities = new ConcurrentHashMap<>(); + private final Map> playerSnapshots = new ConcurrentHashMap<>(); + private final Map> entitySnapshots = new ConcurrentHashMap<>(); + private final Map worldSnapshots = new ConcurrentHashMap<>(); + private final Set enabledProjects = ConcurrentHashMap.newKeySet(); + private final Set blockWarnedProjects = ConcurrentHashMap.newKeySet(); + private final ServerLevel level; + + Box3ScriptSandbox(ServerLevel level) { + this.level = level; + } + + boolean isEnabled(String project) { return project != null && enabledProjects.contains(project); } + + void enable(String project) { enabledProjects.add(project); } + + RestoreSummary disable(String project) { + enabledProjects.remove(project); + return restoreProject(project); + } + + // ── Block tracking ── + + void trackBlock(String project, BlockPos pos) { + if (!isEnabled(project)) return; + Map changes = blockChanges.computeIfAbsent(project, k -> new HashMap<>()); + if (changes.size() >= MAX_BLOCK_CHANGES) return; + changes.putIfAbsent(pos.immutable(), level.getBlockState(pos)); + if (changes.size() >= MAX_BLOCK_CHANGES * WARN_THRESHOLD && blockWarnedProjects.add(project)) { + com.box3lab.box3js.Box3JS.LOGGER.warn("[Sandbox:{}] Block tracking at {}% ({} / {})", + project, (int)(WARN_THRESHOLD * 100), changes.size(), MAX_BLOCK_CHANGES); + } + } + + // ── Entity tracking ── + + void trackEntity(String project, Entity entity) { + if (!isEnabled(project)) return; + spawnedEntities.computeIfAbsent(project, k -> new ArrayList<>()).add(entity); + } + + void trackEntityModify(String project, Entity entity) { + if (!isEnabled(project) || !(entity instanceof LivingEntity)) return; + Map snapshots = entitySnapshots.computeIfAbsent(project, k -> new HashMap<>()); + snapshots.computeIfAbsent(entity.getUUID(), uuid -> EntitySnapshot.capture((LivingEntity) entity)); + } + + // ── Player tracking ── + + void trackPlayer(String project, ServerPlayer player) { + if (!isEnabled(project)) return; + Map snapshots = playerSnapshots.computeIfAbsent(project, k -> new HashMap<>()); + snapshots.computeIfAbsent(player.getUUID(), uuid -> PlayerSnapshot.capture(player, level.getServer())); + } + + // ── World state tracking ── + + void trackWorld(String project) { + if (!isEnabled(project)) return; + worldSnapshots.computeIfAbsent(project, k -> WorldSnapshot.capture(level)); + } + + // ── Restore ── + + RestoreSummary restoreProject(String project) { + int blockCount = 0, entityCount = 0, playerCount = 0; + boolean worldRestored = false; + + Map changes = blockChanges.remove(project); + if (changes != null) { + blockCount = changes.size(); + for (var entry : changes.entrySet()) { + level.setBlock(entry.getKey(), entry.getValue(), 3); + } + } + + List entities = spawnedEntities.remove(project); + if (entities != null) { + entityCount = entities.size(); + for (Entity e : entities) { + if (e.isAlive()) e.discard(); + } + } + + Map pSnapshots = playerSnapshots.remove(project); + if (pSnapshots != null) { + playerCount = pSnapshots.size(); + MinecraftServer server = level.getServer(); + for (var entry : pSnapshots.entrySet()) { + ServerPlayer player = server.getPlayerList().getPlayer(entry.getKey()); + if (player != null) entry.getValue().restore(player, server); + } + } + + Map eSnapshots = entitySnapshots.remove(project); + if (eSnapshots != null) { + for (var entry : eSnapshots.entrySet()) { + ServerPlayer player = level.getServer().getPlayerList().getPlayer(entry.getKey()); + if (player == null) continue; + // Entity snapshots track entities, not players — skip lookup via player list + } + // Actually restore entity snapshots by finding entities still in the world + for (var entry : eSnapshots.entrySet()) { + Entity entity = level.getEntity(entry.getKey()); + if (entity instanceof LivingEntity le) { + entry.getValue().restore(le); + } + } + } + + WorldSnapshot ws = worldSnapshots.remove(project); + if (ws != null) { + ws.restore(level); + worldRestored = true; + } + + blockWarnedProjects.remove(project); + return new RestoreSummary(blockCount, entityCount, playerCount, worldRestored); + } + + void restoreAll() { + for (String project : new HashSet<>(blockChanges.keySet())) restoreProject(project); + for (String project : new HashSet<>(spawnedEntities.keySet())) restoreProject(project); + for (String project : new HashSet<>(playerSnapshots.keySet())) restoreProject(project); + for (String project : new HashSet<>(entitySnapshots.keySet())) restoreProject(project); + for (String project : new HashSet<>(worldSnapshots.keySet())) restoreProject(project); + } + + void inheritEnabled(Box3ScriptSandbox other) { + if (other != null) { + for (String project : other.enabledProjects) enabledProjects.add(project); + } + } + + // ── RestoreSummary ── + + record RestoreSummary(int blocks, int entities, int players, boolean worldRestored) { + boolean hasAny() { return blocks > 0 || entities > 0 || players > 0 || worldRestored; } + + String toMessage() { + StringBuilder sb = new StringBuilder(); + if (blocks > 0) sb.append(blocks).append(" blocks, "); + if (entities > 0) sb.append(entities).append(" entities, "); + if (players > 0) sb.append(players).append(" players, "); + if (worldRestored) sb.append("world state, "); + if (!sb.isEmpty()) sb.setLength(sb.length() - 2); + return sb.toString(); + } + } + + // ═══════════════════════════════════════════════ + // WorldSnapshot + // ═══════════════════════════════════════════════ + + static class WorldSnapshot { + final boolean raining, thundering; + final long dayTime; + final boolean daylightCycle; + final String difficulty; + final double borderSize, borderCenterX, borderCenterZ, borderDamage; + final int borderWarning; + final Map gameRules; + + WorldSnapshot(boolean raining, boolean thundering, long dayTime, boolean daylightCycle, + String difficulty, double borderSize, double borderCenterX, double borderCenterZ, + double borderDamage, int borderWarning, Map gameRules) { + this.raining = raining; this.thundering = thundering; this.dayTime = dayTime; + this.daylightCycle = daylightCycle; this.difficulty = difficulty; + this.borderSize = borderSize; this.borderCenterX = borderCenterX; + this.borderCenterZ = borderCenterZ; this.borderDamage = borderDamage; + this.borderWarning = borderWarning; this.gameRules = gameRules; + } + + static WorldSnapshot capture(ServerLevel level) { + var gamerules = level.getGameRules(); + Map rules = new HashMap<>(); + rules.put("doDaylightCycle", String.valueOf(gamerules.getBoolean(GameRules.RULE_DAYLIGHT))); + rules.put("doWeatherCycle", String.valueOf(gamerules.getBoolean(GameRules.RULE_WEATHER_CYCLE))); + rules.put("keepInventory", String.valueOf(gamerules.getBoolean(GameRules.RULE_KEEPINVENTORY))); + rules.put("doMobSpawning", String.valueOf(gamerules.getBoolean(GameRules.RULE_DOMOBSPAWNING))); + rules.put("doFireTick", String.valueOf(gamerules.getBoolean(GameRules.RULE_DOFIRETICK))); + rules.put("mobGriefing", String.valueOf(gamerules.getBoolean(GameRules.RULE_MOBGRIEFING))); + rules.put("doImmediateRespawn", String.valueOf(gamerules.getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN))); + var border = level.getWorldBorder(); + return new WorldSnapshot( + level.getLevelData().isRaining(), ((ServerLevelData) level.getLevelData()).isThundering(), + level.getDayTime(), gamerules.getBoolean(GameRules.RULE_DAYLIGHT), + level.getDifficulty().getKey(), + border.getSize(), border.getCenterX(), border.getCenterZ(), + border.getDamagePerBlock(), border.getWarningBlocks(), rules + ); + } + + void restore(ServerLevel level) { + level.getLevelData().setRaining(raining); + ((ServerLevelData) level.getLevelData()).setThundering(thundering); + level.setDayTime(dayTime); + level.getGameRules().getRule(GameRules.RULE_DAYLIGHT) + .set(Boolean.parseBoolean(gameRules.get("doDaylightCycle")), level.getServer()); + level.getGameRules().getRule(GameRules.RULE_WEATHER_CYCLE) + .set(Boolean.parseBoolean(gameRules.get("doWeatherCycle")), level.getServer()); + level.getGameRules().getRule(GameRules.RULE_KEEPINVENTORY) + .set(Boolean.parseBoolean(gameRules.get("keepInventory")), level.getServer()); + level.getGameRules().getRule(GameRules.RULE_DOMOBSPAWNING) + .set(Boolean.parseBoolean(gameRules.get("doMobSpawning")), level.getServer()); + level.getGameRules().getRule(GameRules.RULE_DOFIRETICK) + .set(Boolean.parseBoolean(gameRules.get("doFireTick")), level.getServer()); + level.getGameRules().getRule(GameRules.RULE_MOBGRIEFING) + .set(Boolean.parseBoolean(gameRules.get("mobGriefing")), level.getServer()); + level.getGameRules().getRule(GameRules.RULE_DO_IMMEDIATE_RESPAWN) + .set(Boolean.parseBoolean(gameRules.get("doImmediateRespawn")), level.getServer()); + MinecraftServer server = level.getServer(); + net.minecraft.world.Difficulty diff = net.minecraft.world.Difficulty.byName(difficulty); + if (diff != null) server.setDifficulty(diff, true); + var border = level.getWorldBorder(); + border.setSize(borderSize); + border.setCenter(borderCenterX, borderCenterZ); + border.setDamagePerBlock(borderDamage); + border.setWarningBlocks(borderWarning); + } + } + + // ═══════════════════════════════════════════════ + // EntitySnapshot + // ═══════════════════════════════════════════════ + + static class EntitySnapshot { + final float hp, maxHp; + final boolean invisible, glowing, invulnerable, aiEnabled, persistent; + final String nameTag, mainHandItem; + final int fireTicks; + final List effects; + final List tags; + + EntitySnapshot(float hp, float maxHp, boolean invisible, boolean glowing, boolean invulnerable, + boolean aiEnabled, boolean persistent, String nameTag, String mainHandItem, + int fireTicks, List effects, List tags) { + this.hp = hp; this.maxHp = maxHp; this.invisible = invisible; this.glowing = glowing; + this.invulnerable = invulnerable; this.aiEnabled = aiEnabled; this.persistent = persistent; + this.nameTag = nameTag; this.mainHandItem = mainHandItem; this.fireTicks = fireTicks; + this.effects = effects; this.tags = tags; + } + + static EntitySnapshot capture(LivingEntity entity) { + String mainHand = ""; + ItemStack held = entity.getMainHandItem(); + if (!held.isEmpty()) { + ResourceLocation key = entity.registryAccess().registryOrThrow(Registries.ITEM).getKey(held.getItem()); + if (key != null) mainHand = key.toString(); + } + List tagList = new ArrayList<>(entity.getTags()); + boolean ai = entity instanceof Mob m && !m.isNoAi(); + return new EntitySnapshot( + entity.getHealth(), entity.getMaxHealth(), + entity.isInvisible(), entity.isCurrentlyGlowing(), entity.isInvulnerable(), + ai, entity instanceof Mob m && m.isPersistenceRequired(), + entity.getCustomName() != null ? entity.getCustomName().getString() : "", + mainHand, entity.getRemainingFireTicks(), + new ArrayList<>(entity.getActiveEffects()), tagList + ); + } + + void restore(LivingEntity entity) { + if (entity.getMaxHealth() > 0) entity.setHealth(Math.min(hp, entity.getMaxHealth())); + entity.setInvisible(invisible); + entity.setGlowingTag(glowing); + entity.setInvulnerable(invulnerable); + entity.setRemainingFireTicks(fireTicks); + if (!nameTag.isEmpty()) entity.setCustomName(net.minecraft.network.chat.Component.literal(nameTag)); + else entity.setCustomName(null); + if (entity instanceof Mob m) { + m.setNoAi(!aiEnabled); + if (persistent) m.setPersistenceRequired(); + } + entity.removeAllEffects(); + for (var e : effects) entity.addEffect(e); + for (var tag : new ArrayList<>(entity.getTags())) entity.removeTag(tag); + for (var tag : tags) entity.addTag(tag); + if (!mainHandItem.isEmpty()) { + var item = Box3ScriptUtils.lookupItem(mainHandItem); + if (item != null) { + entity.setItemSlot(net.minecraft.world.entity.EquipmentSlot.MAINHAND, new ItemStack(item)); + } + } + } + } + + // ═══════════════════════════════════════════════ + // PlayerSnapshot + // ═══════════════════════════════════════════════ + + static class PlayerSnapshot { + final GameType gameMode; + final boolean mayfly, flying; + final float flySpeed; + final double walkSpeed, jumpPower; + final boolean invisible; + final int opLevel; + final List effects; + // Expanded fields + final List inventory; + final List armor; + final Tag offhand; + final double posX, posY, posZ; + final String dimension; + final int xp; + final int food; + final float saturation; + final String respawnDim; + final int respawnX, respawnY, respawnZ; + + PlayerSnapshot(GameType gameMode, boolean mayfly, boolean flying, float flySpeed, + double walkSpeed, double jumpPower, boolean invisible, int opLevel, + List effects, List inventory, + List armor, Tag offhand, + double posX, double posY, double posZ, String dimension, + int xp, int food, float saturation, + String respawnDim, int respawnX, int respawnY, int respawnZ) { + this.gameMode = gameMode; this.mayfly = mayfly; this.flying = flying; + this.flySpeed = flySpeed; this.walkSpeed = walkSpeed; this.jumpPower = jumpPower; + this.invisible = invisible; this.opLevel = opLevel; this.effects = effects; + this.inventory = inventory; this.armor = armor; this.offhand = offhand; + this.posX = posX; this.posY = posY; this.posZ = posZ; this.dimension = dimension; + this.xp = xp; this.food = food; this.saturation = saturation; + this.respawnDim = respawnDim; this.respawnX = respawnX; this.respawnY = respawnY; + this.respawnZ = respawnZ; + } + + static PlayerSnapshot capture(ServerPlayer player, MinecraftServer server) { + var abilities = player.getAbilities(); + List inv = new ArrayList<>(); + var registryAccess = player.registryAccess(); + for (ItemStack stack : player.getInventory().items) { + inv.add(stack.isEmpty() ? null : stack.save(registryAccess)); + } + List arm = new ArrayList<>(); + for (ItemStack stack : player.getInventory().armor) { + arm.add(stack.isEmpty() ? null : stack.save(registryAccess)); + } + ItemStack offhandStack = player.getInventory().offhand.getFirst(); + Tag oh = offhandStack.isEmpty() ? null : offhandStack.save(registryAccess); + var resp = player.getRespawnPosition(); + String rDim = ""; + int rx = 0, ry = 0, rz = 0; + if (resp != null) { + rDim = player.getRespawnDimension().location().toString(); + rx = resp.getX(); ry = resp.getY(); rz = resp.getZ(); + } + return new PlayerSnapshot( + player.gameMode.getGameModeForPlayer(), + abilities.mayfly, abilities.flying, abilities.getFlyingSpeed(), + player.getAttributeValue(Attributes.MOVEMENT_SPEED), + player.getAttributeValue(Attributes.JUMP_STRENGTH), + player.isInvisible(), server.getProfilePermissions(player.getGameProfile()), + new ArrayList<>(player.getActiveEffects()), + inv, arm, oh, + player.getX(), player.getY(), player.getZ(), + player.level().dimension().location().toString(), + player.experienceLevel, player.getFoodData().getFoodLevel(), + player.getFoodData().getSaturationLevel(), + rDim, rx, ry, rz + ); + } + + void restore(ServerPlayer player, MinecraftServer server) { + player.setGameMode(gameMode); + var abilities = player.getAbilities(); + abilities.mayfly = mayfly; + abilities.flying = flying; + abilities.setFlyingSpeed(flySpeed); + player.onUpdateAbilities(); + player.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(walkSpeed); + player.getAttribute(Attributes.JUMP_STRENGTH).setBaseValue(jumpPower); + player.setInvisible(invisible); + player.removeAllEffects(); + for (var effect : effects) player.addEffect(effect); + if (opLevel > 0) server.getPlayerList().op(player.getGameProfile()); + else server.getPlayerList().deop(player.getGameProfile()); + // Restore inventory + var registryAccess = player.registryAccess(); + if (inventory != null) { + for (int i = 0; i < inventory.size() && i < player.getInventory().items.size(); i++) { + Tag tag = inventory.get(i); + player.getInventory().items.set(i, tag != null + ? ItemStack.parse(registryAccess, tag).orElse(ItemStack.EMPTY) + : ItemStack.EMPTY); + } + } + if (armor != null) { + for (int i = 0; i < armor.size() && i < player.getInventory().armor.size(); i++) { + Tag tag = armor.get(i); + player.getInventory().armor.set(i, tag != null + ? ItemStack.parse(registryAccess, tag).orElse(ItemStack.EMPTY) + : ItemStack.EMPTY); + } + } + if (offhand != null) { + player.getInventory().offhand.set(0, ItemStack.parse(registryAccess, offhand) + .orElse(ItemStack.EMPTY)); + } + // Restore position / dimension + if (dimension != null && !dimension.equals(player.level().dimension().location().toString())) { + ResourceLocation rl = ResourceLocation.tryParse(dimension); + if (rl != null) { + ServerLevel target = server.getLevel(ResourceKey.create(Registries.DIMENSION, rl)); + if (target != null) player.teleportTo(target, posX, posY, posZ, player.getYRot(), player.getXRot()); + else player.teleportTo(posX, posY, posZ); + } else { + player.teleportTo(posX, posY, posZ); + } + } else { + player.teleportTo(posX, posY, posZ); + } + player.experienceLevel = xp; + player.getFoodData().setFoodLevel(food); + player.getFoodData().setSaturation(saturation); + // Restore respawn point + if (!respawnDim.isEmpty()) { + ResourceLocation rl = ResourceLocation.tryParse(respawnDim); + if (rl != null) { + player.setRespawnPosition(ResourceKey.create(Registries.DIMENSION, rl), + new BlockPos(respawnX, respawnY, respawnZ), 0, true, false); + } + } + } + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java new file mode 100644 index 00000000..264b3fea --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java @@ -0,0 +1,36 @@ +package com.box3lab.box3js.script; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +public class Box3ScriptTemplate { + + private static final String[] FILES = { + "gitignore.template", + "package.json", + "tsconfig.json", + "src/app.ts", + "types/globals.d.ts", + }; + + public static void copyTo(Path projectDir, String projectName) throws IOException { + Files.createDirectories(projectDir); + for (String relPath : FILES) { + String destName = relPath.equals("gitignore.template") ? ".gitignore" : relPath; + Path dest = projectDir.resolve(destName); + Files.createDirectories(dest.getParent()); + String resourcePath = "/assets/box3js/template/" + relPath; + try (InputStream in = Box3ScriptTemplate.class.getResourceAsStream(resourcePath)) { + if (in == null) throw new IOException("Template file not found: " + resourcePath); + Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING); + } + if (relPath.equals("src/app.ts")) { + String content = Files.readString(dest); + Files.writeString(dest, content.replace("PROJECT_NAME", projectName)); + } + } + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptUtils.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptUtils.java new file mode 100644 index 00000000..8c4ce57d --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptUtils.java @@ -0,0 +1,85 @@ +package com.box3lab.box3js.script; + +import net.minecraft.core.Holder; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.Block; + +public class Box3ScriptUtils { + + public static Item lookupItem(String id) { + ResourceLocation rl = ResourceLocation.tryParse(id); + if (rl == null) return null; + return BuiltInRegistries.ITEM.getOptional(rl).orElse(null); + } + + public static EntityType lookupEntityType(String id) { + ResourceLocation rl = ResourceLocation.tryParse(id); + if (rl == null) return null; + return BuiltInRegistries.ENTITY_TYPE.getOptional(rl).orElse(null); + } + + public static Block lookupBlock(String id) { + ResourceLocation rl = ResourceLocation.tryParse(id); + if (rl == null) return null; + return BuiltInRegistries.BLOCK.getOptional(rl).orElse(null); + } + + public static Holder lookupMobEffect(String id) { + ResourceLocation rl = ResourceLocation.tryParse(id); + if (rl == null) return null; + return BuiltInRegistries.MOB_EFFECT.getHolder(rl).orElse(null); + } + + public static Holder lookupSoundEvent(String id) { + ResourceLocation rl = ResourceLocation.tryParse(id); + if (rl == null) return null; + return BuiltInRegistries.SOUND_EVENT.getHolder(rl).orElse(null); + } + + public static ParticleOptions lookupParticle(String id) { + ResourceLocation rl = ResourceLocation.tryParse(id); + if (rl == null) return null; + var type = BuiltInRegistries.PARTICLE_TYPE.getOptional(rl); + if (type.isEmpty()) return null; + try { + return (ParticleOptions) type.get(); + } catch (ClassCastException ignored) { + return null; + } + } + + public static Holder lookupAttribute(String id) { + ResourceLocation rl = ResourceLocation.tryParse(id); + if (rl == null) return null; + return BuiltInRegistries.ATTRIBUTE.getHolder(rl).orElse(null); + } + + public static boolean coerceBool(Object v) { + return v instanceof Boolean b ? b : Boolean.parseBoolean(v.toString()); + } + + static String resolveScoreName(Object entityOrName) { + if (entityOrName instanceof String s) return s; + if (entityOrName instanceof Box3JSEntity e) return e.getEntity().getScoreboardName(); + if (entityOrName instanceof ServerPlayer sp) return sp.getScoreboardName(); + return null; + } + + public static void lookAt(Entity entity, double x, double y, double z) { + double dx = x - entity.getX(); + double dy = y - entity.getEyeY(); + double dz = z - entity.getZ(); + double hd = Math.sqrt(dx * dx + dz * dz); + entity.setYRot((float) (Math.toDegrees(Math.atan2(dz, dx)) - 90.0)); + entity.setXRot((float) (-Math.toDegrees(Math.atan2(dy, hd)))); + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptWatcher.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptWatcher.java new file mode 100644 index 00000000..66866334 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptWatcher.java @@ -0,0 +1,157 @@ +package com.box3lab.box3js.script; + +import com.box3lab.box3js.Box3JS; +import net.minecraft.server.MinecraftServer; + +import java.io.IOException; +import java.nio.file.*; +import java.util.Map; +import java.util.concurrent.*; + +import static java.nio.file.StandardWatchEventKinds.*; + +class Box3ScriptWatcher { + + private static final long DEBOUNCE_MS = 2000; + + private final MinecraftServer server; + private final Box3ScriptEngine engine; + private final Box3ScriptConfig config; + private WatchService watchService; + private ScheduledExecutorService scheduler; + private final Map> pending = new ConcurrentHashMap<>(); + private volatile boolean running; + + Box3ScriptWatcher(MinecraftServer server) { + this.server = server; + this.engine = Box3ScriptEngine.get(); + this.config = Box3ScriptConfig.get(); + } + + boolean isRunning() { return running; } + + void start() { + if (running) return; + try { + watchService = FileSystems.getDefault().newWatchService(); + Path scriptDir = config.getScriptDir(server); + if (!Files.exists(scriptDir)) { + Files.createDirectories(scriptDir); + } + registerDistDirs(scriptDir); + scheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "Box3Script-Watcher"); + t.setDaemon(true); + return t; + }); + running = true; + new Thread(this::pollLoop, "Box3Script-Watcher-Poll").start(); + Box3JS.LOGGER.info("File watcher started (dist/ only) on {}", scriptDir); + } catch (IOException e) { + Box3JS.LOGGER.error("Failed to start watcher", e); + } + } + + void stop() { + running = false; + if (watchService != null) { + try { watchService.close(); } catch (IOException ignored) {} + watchService = null; + } + if (scheduler != null) { + scheduler.shutdownNow(); + scheduler = null; + } + pending.clear(); + Box3JS.LOGGER.info("File watcher stopped"); + } + + /** Register only dist/ directories under each project. */ + private void registerDistDirs(Path scriptDir) throws IOException { + if (!Files.isDirectory(scriptDir)) return; + try (var dirs = Files.list(scriptDir)) { + dirs.filter(Files::isDirectory).forEach(projectDir -> { + Path distDir = projectDir.resolve("dist"); + if (Files.isDirectory(distDir)) { + try { + distDir.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE); + Box3JS.LOGGER.info("Watching: {}", distDir); + } catch (IOException e) { + Box3JS.LOGGER.error("Failed to register watch: {}", distDir, e); + } + } + }); + } + } + + private void pollLoop() { + while (running) { + WatchKey key; + try { key = watchService.poll(1, TimeUnit.SECONDS); } + catch (InterruptedException e) { break; } + catch (ClosedWatchServiceException e) { break; } + if (key == null) continue; + + Path dir = (Path) key.watchable(); + String project = dir.getParent().getFileName().toString(); + for (WatchEvent event : key.pollEvents()) { + WatchEvent.Kind kind = event.kind(); + if (kind == OVERFLOW) continue; + String fileName = ((Path) event.context()).toString(); + // Only react to JS output files in dist/ + if (!fileName.endsWith(".js")) continue; + + if (kind == ENTRY_DELETE && fileName.equals("app.js")) { + // dist/app.js deleted — allow rebuild to recreate it + // debounce will pick up the next CREATE/MODIFY + } + debounceReload(project); + } + boolean valid = key.reset(); + if (!valid) { + // dist/ was deleted — try re-registering + Box3JS.LOGGER.warn("Watch key invalid for {}/dist, attempting re-register", project); + retryRegister(project); + } + } + } + + private void retryRegister(String project) { + try { + Path distDir = config.getScriptDir(server).resolve(project).resolve("dist"); + Thread.sleep(2000); // wait for rebuild tool to recreate + if (Files.isDirectory(distDir)) { + distDir.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE); + Box3JS.LOGGER.info("Re-registered watch: {}", distDir); + } + } catch (IOException | InterruptedException ignored) {} + } + + private void debounceReload(String project) { + if (!config.isEnabled(project)) return; + synchronized (pending) { + ScheduledFuture existing = pending.remove(project); + if (existing != null) existing.cancel(false); + pending.put(project, scheduler.schedule(() -> { + pending.remove(project); + reloadProject(project); + }, DEBOUNCE_MS, TimeUnit.MILLISECONDS)); + } + } + + private void reloadProject(String project) { + if (!config.isEnabled(project)) return; + try { + engine.setCurrentProject(project); + try { + engine.removeProject(project); + engine.eval("require('./app')"); + Box3JS.LOGGER.info("Watcher reloaded: {}", project); + } finally { + engine.setCurrentProject(null); + } + } catch (Exception e) { + Box3JS.LOGGER.error("Watcher reload failed for {}", project, e); + } + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameBounds3.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameBounds3.java new file mode 100644 index 00000000..960e50eb --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameBounds3.java @@ -0,0 +1,28 @@ +package com.box3lab.box3js.script; + +public class GameBounds3 { + + public GameVector3 lo, hi; + + public GameBounds3(GameVector3 lo, GameVector3 hi) { + this.lo = lo; + this.hi = hi; + } + + public boolean intersects(GameBounds3 other) { + return !(hi.x < other.lo.x || lo.x > other.hi.x || + hi.y < other.lo.y || lo.y > other.hi.y || + hi.z < other.lo.z || lo.z > other.hi.z); + } + + public boolean contains(GameVector3 v) { + return v.x >= lo.x && v.x <= hi.x && + v.y >= lo.y && v.y <= hi.y && + v.z >= lo.z && v.z <= hi.z; + } + + @Override + public String toString() { + return "GameBounds3(" + lo + ", " + hi + ")"; + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameQuaternion.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameQuaternion.java new file mode 100644 index 00000000..3dcd7133 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameQuaternion.java @@ -0,0 +1,192 @@ +package com.box3lab.box3js.script; + +public class GameQuaternion { + + public double w, x, y, z; + + public GameQuaternion() { this(1, 0, 0, 0); } + + public GameQuaternion(double w, double x, double y, double z) { + this.w = w; this.x = x; this.y = y; this.z = z; + } + + public GameQuaternion set(double w, double x, double y, double z) { + this.w = w; this.x = x; this.y = y; this.z = z; return this; + } + + public GameQuaternion copy(GameQuaternion v) { + this.w = v.w; this.x = v.x; this.y = v.y; this.z = v.z; return this; + } + + public GameQuaternion clone() { + return new GameQuaternion(w, x, y, z); + } + + public GameQuaternion add(GameQuaternion v) { + return new GameQuaternion(w + v.w, x + v.x, y + v.y, z + v.z); + } + + public GameQuaternion sub(GameQuaternion v) { + return new GameQuaternion(w - v.w, x - v.x, y - v.y, z - v.z); + } + + /** Hamilton product: this * q */ + public GameQuaternion mul(GameQuaternion q) { + return new GameQuaternion( + w * q.w - x * q.x - y * q.y - z * q.z, + w * q.x + x * q.w + y * q.z - z * q.y, + w * q.y - x * q.z + y * q.w + z * q.x, + w * q.z + x * q.y - y * q.x + z * q.w + ); + } + + /** Conjugate (inverse for unit quaternions) */ + public GameQuaternion inv() { + return new GameQuaternion(w, -x, -y, -z); + } + + /** Division: this * q^-1 */ + public GameQuaternion div(GameQuaternion q) { + double denom = q.w * q.w + q.x * q.x + q.y * q.y + q.z * q.z; + if (denom == 0) return clone(); + GameQuaternion qi = new GameQuaternion(q.w / denom, -q.x / denom, -q.y / denom, -q.z / denom); + return mul(qi); + } + + public double dot(GameQuaternion q) { + return w * q.w + x * q.x + y * q.y + z * q.z; + } + + public double mag() { + return Math.sqrt(w * w + x * x + y * y + z * z); + } + + public double sqrMag() { + return w * w + x * x + y * y + z * z; + } + + public GameQuaternion normalize() { + double m = mag(); + return m == 0 ? new GameQuaternion(1, 0, 0, 0) : new GameQuaternion(w / m, x / m, y / m, z / m); + } + + public GameQuaternion slerp(GameQuaternion q, double t) { + double cosTheta = dot(q); + GameQuaternion q2 = q; + if (cosTheta < 0) { cosTheta = -cosTheta; q2 = new GameQuaternion(-q.w, -q.x, -q.y, -q.z); } + if (cosTheta > 0.9995) { + GameQuaternion r = new GameQuaternion( + w + (q2.w - w) * t, x + (q2.x - x) * t, + y + (q2.y - y) * t, z + (q2.z - z) * t); + return r.normalize(); + } + double theta = Math.acos(cosTheta); + double sinTheta = Math.sin(theta); + double a = Math.sin((1 - t) * theta) / sinTheta; + double b = Math.sin(t * theta) / sinTheta; + return new GameQuaternion( + a * w + b * q2.w, a * x + b * q2.x, + a * y + b * q2.y, a * z + b * q2.z); + } + + /** Angle in radians between this and q */ + public double angle(GameQuaternion q) { + double d = dot(q); + if (d > 1) d = 1; if (d < -1) d = -1; + return 2 * Math.acos(Math.abs(d)); + } + + /** Returns {angle, axis} for this quaternion */ + public AxisAngle getAxisAngle() { + GameQuaternion q = normalize(); + double angle = 2 * Math.acos(q.w); + double s = Math.sqrt(1 - q.w * q.w); + GameVector3 axis; + if (s < 1e-6) { + axis = new GameVector3(1, 0, 0); + } else { + axis = new GameVector3(q.x / s, q.y / s, q.z / s); + } + return new AxisAngle(angle, axis); + } + + /** Return type for getAxisAngle() — public fields accessible from JS */ + public static class AxisAngle { + public double angle; + public GameVector3 axis; + AxisAngle(double angle, GameVector3 axis) { this.angle = angle; this.axis = axis; } + } + + public boolean equals(GameQuaternion v) { + return Math.abs(w - v.w) < 1e-6 && Math.abs(x - v.x) < 1e-6 && + Math.abs(y - v.y) < 1e-6 && Math.abs(z - v.z) < 1e-6; + } + + // ---- Rotations ---- + + public GameQuaternion rotateX(double rad) { + double half = rad / 2; + GameQuaternion rx = new GameQuaternion(Math.cos(half), Math.sin(half), 0, 0); + return rx.mul(this); + } + + public GameQuaternion rotateY(double rad) { + double half = rad / 2; + GameQuaternion ry = new GameQuaternion(Math.cos(half), 0, Math.sin(half), 0); + return ry.mul(this); + } + + public GameQuaternion rotateZ(double rad) { + double half = rad / 2; + GameQuaternion rz = new GameQuaternion(Math.cos(half), 0, 0, Math.sin(half)); + return rz.mul(this); + } + + // ---- Static constructors ---- + + public static GameQuaternion fromAxisAngle(GameVector3 axis, double rad) { + double half = rad / 2; + double s = Math.sin(half); + GameVector3 n = axis.normalize(); + return new GameQuaternion(Math.cos(half), n.x * s, n.y * s, n.z * s); + } + + /** YZX Euler order: rotate Y then Z then X */ + public static GameQuaternion fromEuler(double x, double y, double z) { + double cx = Math.cos(x / 2), sx = Math.sin(x / 2); + double cy = Math.cos(y / 2), sy = Math.sin(y / 2); + double cz = Math.cos(z / 2), sz = Math.sin(z / 2); + return new GameQuaternion( + cy * cz * cx + sy * sz * sx, + cy * cz * sx - sy * sz * cx, + cy * sz * cx + sy * cz * sx, + sy * cz * cx - cy * sz * sx + ); + } + + /** Shortest-arc quaternion rotating from vector a to b */ + public static GameQuaternion rotationBetween(GameVector3 a, GameVector3 b) { + GameVector3 an = a.normalize(); + GameVector3 bn = b.normalize(); + double dot = an.dot(bn); + if (dot > 0.99999) return new GameQuaternion(1, 0, 0, 0); + if (dot < -0.99999) { + GameVector3 axis = Math.abs(an.x) < 0.9 + ? new GameVector3(1, 0, 0).add(an).normalize() + : new GameVector3(0, 1, 0).add(an).normalize(); + return new GameQuaternion(0, axis.x, axis.y, axis.z); + } + GameVector3 axis = new GameVector3( + an.y * bn.z - an.z * bn.y, + an.z * bn.x - an.x * bn.z, + an.x * bn.y - an.y * bn.x + ); + double s = Math.sqrt((1 + dot) * 2); + return new GameQuaternion(s / 2, axis.x / s, axis.y / s, axis.z / s); + } + + @Override + public String toString() { + return "GameQuaternion(" + w + ", " + x + ", " + y + ", " + z + ")"; + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameRGBAColor.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameRGBAColor.java new file mode 100644 index 00000000..3354981d --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameRGBAColor.java @@ -0,0 +1,88 @@ +package com.box3lab.box3js.script; + +public class GameRGBAColor { + + public double r, g, b, a; + + public GameRGBAColor(double r, double g, double b, double a) { + this.r = r; this.g = g; this.b = b; this.a = a; + } + + public GameRGBAColor set(double r, double g, double b, double a) { + this.r = r; this.g = g; this.b = b; this.a = a; return this; + } + + public GameRGBAColor copy(GameRGBAColor c) { + this.r = c.r; this.g = c.g; this.b = c.b; this.a = c.a; return this; + } + + public GameRGBAColor clone() { + return new GameRGBAColor(r, g, b, a); + } + + public GameRGBAColor add(GameRGBAColor rgba) { + return new GameRGBAColor(r + rgba.r, g + rgba.g, b + rgba.b, a + rgba.a); + } + + public GameRGBAColor sub(GameRGBAColor rgba) { + return new GameRGBAColor(r - rgba.r, g - rgba.g, b - rgba.b, a - rgba.a); + } + + public GameRGBAColor mul(GameRGBAColor rgba) { + return new GameRGBAColor(r * rgba.r, g * rgba.g, b * rgba.b, a * rgba.a); + } + + public GameRGBAColor div(GameRGBAColor rgba) { + return new GameRGBAColor( + rgba.r == 0 ? 0 : r / rgba.r, + rgba.g == 0 ? 0 : g / rgba.g, + rgba.b == 0 ? 0 : b / rgba.b, + rgba.a == 0 ? 0 : a / rgba.a); + } + + // Mutating variants (return this) + public GameRGBAColor addEq(GameRGBAColor rgba) { + r += rgba.r; g += rgba.g; b += rgba.b; a += rgba.a; return this; + } + + public GameRGBAColor subEq(GameRGBAColor rgba) { + r -= rgba.r; g -= rgba.g; b -= rgba.b; a -= rgba.a; return this; + } + + public GameRGBAColor mulEq(GameRGBAColor rgba) { + r *= rgba.r; g *= rgba.g; b *= rgba.b; a *= rgba.a; return this; + } + + public GameRGBAColor divEq(GameRGBAColor rgba) { + if (rgba.r != 0) r /= rgba.r; + if (rgba.g != 0) g /= rgba.g; + if (rgba.b != 0) b /= rgba.b; + if (rgba.a != 0) a /= rgba.a; + return this; + } + + public GameRGBAColor lerp(GameRGBAColor rgba, double n) { + return new GameRGBAColor( + r + (rgba.r - r) * n, g + (rgba.g - g) * n, + b + (rgba.b - b) * n, a + (rgba.a - a) * n); + } + + public boolean equals(GameRGBAColor rgba) { + return Math.abs(r - rgba.r) < 1e-6 && Math.abs(g - rgba.g) < 1e-6 && + Math.abs(b - rgba.b) < 1e-6 && Math.abs(a - rgba.a) < 1e-6; + } + + /** Blend this RGBA color onto an RGB background, returning the displayed GameRGBColor */ + public GameRGBColor blendEq(GameRGBColor rgb) { + double alpha = Math.max(0, Math.min(1, a)); + return new GameRGBColor( + r * alpha + rgb.r * (1 - alpha), + g * alpha + rgb.g * (1 - alpha), + b * alpha + rgb.b * (1 - alpha)); + } + + @Override + public String toString() { + return "GameRGBAColor(" + r + ", " + g + ", " + b + ", " + a + ")"; + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameRGBColor.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameRGBColor.java new file mode 100644 index 00000000..bca71a1b --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameRGBColor.java @@ -0,0 +1,23 @@ +package com.box3lab.box3js.script; + +public class GameRGBColor { + + public double r, g, b; + + public GameRGBColor(double r, double g, double b) { + this.r = r; this.g = g; this.b = b; + } + + public GameRGBColor lerp(GameRGBColor o, double n) { + return new GameRGBColor(r + (o.r - r) * n, g + (o.g - g) * n, b + (o.b - b) * n); + } + + public static GameRGBColor random() { + return new GameRGBColor(Math.random(), Math.random(), Math.random()); + } + + @Override + public String toString() { + return "GameRGBColor(" + r + ", " + g + ", " + b + ")"; + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameVector3.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameVector3.java new file mode 100644 index 00000000..2654a7c0 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameVector3.java @@ -0,0 +1,69 @@ +package com.box3lab.box3js.script; + +public class GameVector3 { + + public double x, y, z; + + public GameVector3() { this(0, 0, 0); } + + public GameVector3(double x, double y, double z) { + this.x = x; this.y = y; this.z = z; + } + + public GameVector3 set(double x, double y, double z) { + this.x = x; this.y = y; this.z = z; return this; + } + + public GameVector3 add(GameVector3 v) { + return new GameVector3(x + v.x, y + v.y, z + v.z); + } + + public GameVector3 sub(GameVector3 v) { + return new GameVector3(x - v.x, y - v.y, z - v.z); + } + + public GameVector3 scale(double n) { + return new GameVector3(x * n, y * n, z * n); + } + + public double dot(GameVector3 v) { + return x * v.x + y * v.y + z * v.z; + } + + public double mag() { + return Math.sqrt(x * x + y * y + z * z); + } + + public double sqrMag() { return x * x + y * y + z * z; } + + public GameVector3 normalize() { + double m = mag(); + return m == 0 ? new GameVector3(0, 0, 0) : scale(1.0 / m); + } + + public double distance(GameVector3 v) { + double dx = x - v.x, dy = y - v.y, dz = z - v.z; + return Math.sqrt(dx * dx + dy * dy + dz * dz); + } + + public GameVector3 lerp(GameVector3 v, double n) { + return new GameVector3(x + (v.x - x) * n, y + (v.y - y) * n, z + (v.z - z) * n); + } + + public boolean equals(GameVector3 v) { + return x == v.x && y == v.y && z == v.z; + } + + public static GameVector3 fromPolar(double mag, double phi, double theta) { + return new GameVector3( + mag * Math.cos(phi) * Math.cos(theta), + mag * Math.sin(theta), + mag * Math.sin(phi) * Math.cos(theta) + ); + } + + @Override + public String toString() { + return "GameVector3(" + x + ", " + y + ", " + z + ")"; + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/build.mjs b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/build.mjs new file mode 100644 index 00000000..7857bebe --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/build.mjs @@ -0,0 +1,168 @@ +import * as esbuild from "esbuild"; +import { resolve, dirname, relative } from "path"; +import { fileURLToPath } from "url"; +import { + writeFileSync, + mkdirSync, + readdirSync, + statSync, + rmSync, + existsSync, + readFileSync, + watch as fsWatch, +} from "fs"; +import babel from "@babel/core"; + +// Build script entry paths / 构建脚本入口路径 +const __dirname = dirname(fileURLToPath(import.meta.url)); +const srcDir = resolve(__dirname, "src"); +const tempDir = resolve(__dirname, ".temp"); +const distDir = resolve(__dirname, "dist"); +const distFile = resolve(distDir, "app.js"); + +// Watch mode flag: `node build.mjs --watch` / 监听模式开关 +const isWatchMode = process.argv.includes("--watch"); + +// Rhino parser cannot handle regex literal in bundled helper, +// so we replace that specific generated pattern after bundling. +// Rhino 解析器不接受该 helper 正则字面量,因此打包后做定向替换。 +const BAD_REGEX = + /\/\^\(\?:Ui\|I\)nt\(\?:8\|16\|32\)\(\?:Clamped\)\?Array\$\/\.test\((\w+)\)/g; +function typedArrayCheck(_, varName) { + return `(${varName} === "Int8Array" || ${varName} === "Uint8Array" || ${varName} === "Uint8ClampedArray" || ${varName} === "Int16Array" || ${varName} === "Uint16Array" || ${varName} === "Int32Array" || ${varName} === "Uint32Array")`; +} + +// Remove temp directory safely / 安全清理临时目录 +function cleanupTempDir() { + if (existsSync(tempDir)) rmSync(tempDir, { recursive: true, force: true }); +} + +// Recursively collect all .ts files under src / 递归收集 src 下所有 .ts 文件 +function collectTsFiles(dir, out = []) { + for (const name of readdirSync(dir)) { + const full = resolve(dir, name); + const st = statSync(full); + if (st.isDirectory()) collectTsFiles(full, out); + else if (name.endsWith(".ts")) out.push(full); + } + return out; +} + +// Step 1: Babel transpile TS -> temp JS / 第一步:使用 Babel 将 TS 转为临时 JS +async function transpileTsToTemp() { + cleanupTempDir(); + mkdirSync(tempDir, { recursive: true }); + mkdirSync(distDir, { recursive: true }); + + for (const inputPath of collectTsFiles(srcDir)) { + const rel = relative(srcDir, inputPath); + const out = resolve(tempDir, rel.replace(/\.ts$/, ".js")); + + const result = await babel.transformFileAsync(inputPath, { + presets: [ + [ + "@babel/preset-env", + { + targets: { rhino: "1.9.1" }, + modules: false, + bugfixes: true, + loose: true, + }, + ], + "@babel/preset-typescript", + ], + configFile: false, + babelrc: false, + comments: false, + }); + + if (!result?.code) throw new Error(`Babel transform failed: ${rel}`); + mkdirSync(dirname(out), { recursive: true }); + writeFileSync(out, result.code, "utf-8"); + } +} + +// Step 2: Bundle and sanitize regex literal for Rhino parser / 第二步:打包并修正 Rhino 不支持的 helper 正则字面量 +async function bundleAndSanitize() { + await esbuild.build({ + entryPoints: [resolve(tempDir, "app.js")], + outfile: distFile, + bundle: true, + format: "cjs", + platform: "neutral", + target: ["rhino1.9.1"], + minify: true, + write: true, + logLevel: "info", + }); + + const code = readFileSync(distFile, "utf-8"); + const sanitized = code.replace(BAD_REGEX, typedArrayCheck); + writeFileSync(distFile, sanitized, "utf-8"); +} + +// Single build pipeline / 单次构建流程 +async function buildOnce() { + await transpileTsToTemp(); + await bundleAndSanitize(); +} + +if (!isWatchMode) { + // One-shot build mode / 单次构建模式 + try { + await buildOnce(); + } finally { + cleanupTempDir(); + } +} else { + // Watch mode with debounce + serial rebuild / 监听模式 + let timer = null; + let building = false; + let pending = false; + let closing = false; + + // Graceful shutdown: clean temp then exit / 先清理临时目录再退出 + const closeWatch = () => { + if (closing) return; + closing = true; + cleanupTempDir(); + process.exit(0); + }; + + process.on("SIGINT", closeWatch); + process.on("SIGTERM", closeWatch); + + const runBuild = async () => { + if (building) { + pending = true; + return; + } + + building = true; + try { + await buildOnce(); + } catch (err) { + console.error("❌ Build failed:", err); + } finally { + building = false; + if (pending) { + pending = false; + void runBuild(); + } + } + }; + + await runBuild(); + console.log("👀 Watching src/**/*.ts for changes..."); + + fsWatch(srcDir, { recursive: true }, (_eventType, filename) => { + if (!filename || !filename.endsWith(".ts")) return; + if (timer) clearTimeout(timer); + timer = setTimeout(() => { + console.log(`♻️ Rebuilding: ${filename}`); + void runBuild(); + }, 120); + }); + + await new Promise(() => {}); +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/gitignore.template b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/gitignore.template new file mode 100644 index 00000000..c2b6ab3d --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/gitignore.template @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.env +.temp/ \ No newline at end of file diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/package.json b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/package.json new file mode 100644 index 00000000..be5865fd --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/package.json @@ -0,0 +1,18 @@ +{ + "name": "box3js", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "node build.mjs --watch", + "build": "node build.mjs", + "check": "tsc --noEmit" + }, + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/preset-env": "^7.29.5", + "@babel/preset-typescript": "^7.28.5", + "esbuild": "^0.28.0", + "typescript": "^6.0.3" + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/app.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/app.ts new file mode 100644 index 00000000..040a2c90 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/app.ts @@ -0,0 +1,38 @@ +// ═══════════════════════════════════════════════════ +// PROJECT_NAME +// ═══════════════════════════════════════════════════ + +// 玩家加入时欢迎 +world.onPlayerJoin(function (entity: GameEntity) { + var p = entity.player; + if (!p) return; + world.say("§e" + p.name + " §7进入了服务器"); + p.directMessage("§6欢迎来到 §ePROJECT_NAME §6!"); + p.directMessage("§7输入 §e!hello §7打个招呼吧"); +}); + +// 聊天命令 +world.onChat(function (entity: GameEntity, message: string, _tick: number) { + var p = entity.player; + if (!p) return; + + if (message === "!hello") { + world.say("§e" + p.name + "§7: Hello World!"); + } +}); + +// 每 5 秒公告一次 +var announceTicks = 0; +world.onTick(function () { + announceTicks++; + if (announceTicks >= 100) { + announceTicks = 0; + var players = world.querySelectorAll("*"); + for (var i = 0; i < players.length; i++) { + var p = players[i].player; + if (p) p.actionBar("§a⚡ PROJECT_NAME 运行中 §7| §f" + players.length + " §7人在线"); + } + } +}); + +console.log("[PROJECT_NAME] loaded — Hello World!"); diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/tsconfig.json b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/tsconfig.json new file mode 100644 index 00000000..c3666879 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ESNext"], + "strict": true, + "noEmit": true, + "isolatedModules": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts", "types/**/*.d.ts"] +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/globals.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/globals.d.ts new file mode 100644 index 00000000..ca416e00 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/globals.d.ts @@ -0,0 +1,2237 @@ +// ================================================================ +// §1 Math Types — 数学类型 +// ================================================================ + +/** + * 三维向量 + * A 3‑dimensional vector with double‑precision components. + * + * @remarks + * 所有坐标使用世界坐标 (方块坐标, 非像素)。 + * All coordinates are in world space (block coordinates, not pixels). + */ +declare class GameVector3 { + /** X 分量 — X component (east‑west) */ + x: number; + /** Y 分量 — Y component (up‑down) */ + y: number; + /** Z 分量 — Z component (north‑south) */ + z: number; + + /** + * 创建一个零向量 (0, 0, 0)。 + * Creates a zero vector at origin. + */ + constructor(); + + /** + * 创建一个指定坐标的向量。 + * Creates a vector with the given coordinates. + * @param x - X 坐标 / X coordinate + * @param y - Y 坐标 / Y coordinate + * @param z - Z 坐标 / Z coordinate + */ + constructor(x: number, y: number, z: number); + + /** + * 设置向量的 X / Y / Z 分量 (会改变调用者自身)。 + * Sets all three components in‑place (mutates the vector). + * @returns 调用者本身 / this vector + */ + set(x: number, y: number, z: number): GameVector3; + + /** + * 向量加法: this + v。 + * Vector addition: this + v. + * @returns 一个新向量 / a new vector + */ + add(v: GameVector3): GameVector3; + + /** + * 向量减法: this - v。 + * Vector subtraction: this - v. + * @returns 一个新向量 / a new vector + */ + sub(v: GameVector3): GameVector3; + + /** + * 标量乘法: 每个分量乘以 n。 + * Scalar multiplication: each component multiplied by n. + * @returns 一个新向量 / a new vector + */ + scale(n: number): GameVector3; + + /** + * 点积 (内积): this · v。 + * Dot (inner) product: this · v. + */ + dot(v: GameVector3): number; + + /** + * 向量长度 (模)。 + * Magnitude (length) of this vector. + */ + mag(): number; + + /** + * 向量长度的平方 (比 mag() 更快)。 + * Squared magnitude — faster than mag() when comparing distances. + */ + sqrMag(): number; + + /** + * 单位化: 返回方向相同、长度为 1 的新向量。 + * Normalizes this vector; returns a unit vector in the same direction. + * 零向量会返回 (0,0,0)。 + */ + normalize(): GameVector3; + + /** + * 计算 this 与 v 之间的欧几里得距离。 + * Euclidean distance between this and v. + */ + distance(v: GameVector3): number; + + /** + * 线性插值: 在 this 和 v 之间按比率 n 插值。 + * Linear interpolation between this and v by ratio n. + * @param n - 插值比率 (0=this, 1=v) / interpolation factor + */ + lerp(v: GameVector3, n: number): GameVector3; + + /** + * 检查两个向量的所有分量是否完全相等。 + * Returns true if all components are exactly equal. + */ + equals(v: GameVector3): boolean; + + /** + * 从球坐标创建向量。 + * Creates a vector from spherical coordinates. + * @param mag - 半径 / radius (magnitude) + * @param phi - 方位角 / azimuth angle (radians, horizontal rotation around Y) + * @param theta - 仰角 / elevation angle (radians, from horizontal plane) + */ + static fromPolar(mag: number, phi: number, theta: number): GameVector3; + + /** 返回 "(x, y, z)" 格式的字符串表示。 */ + toString(): string; +} + +// ────────────────────────────────────────────── + +/** + * 三维轴对齐包围盒 (AABB)。 + * Axis‑aligned 3‑dimensional bounding box. + * + * @remarks + * 由两个对角顶点 lo (最小角) 和 hi (最大角) 定义。 + * Defined by two opposing corners: lo (minimum corner) and hi (maximum corner). + */ +declare class GameBounds3 { + /** 最小角 (三个分量均为最小值)。Lower/minimum corner. */ + lo: GameVector3; + /** 最大角 (三个分量均为最大值)。Upper/maximum corner. */ + hi: GameVector3; + + /** + * 用两个对角顶点构造包围盒。 + * Constructs bounds from two opposing corners. + */ + constructor(lo: GameVector3, hi: GameVector3); + + /** + * 判断当前包围盒是否与 other 相交。 + * Returns true if this bounds intersects with other. + */ + intersects(other: GameBounds3): boolean; + + /** + * 判断点 v 是否位于包围盒内部 (含边界)。 + * Returns true if point v is inside (or on the boundary of) this bounds. + */ + contains(v: GameVector3): boolean; + + toString(): string; +} + +// ────────────────────────────────────────────── + +/** + * RGB 颜色 (三个通道, 每通道 0.0‑1.0)。 + * An RGB color with three channels ranging from 0.0 to 1.0. + */ +declare class GameRGBColor { + /** 红色通道 (0.0‑1.0)。Red channel. */ + r: number; + /** 绿色通道 (0.0‑1.0)。Green channel. */ + g: number; + /** 蓝色通道 (0.0‑1.0)。Blue channel. */ + b: number; + + /** + * 用指定的 R / G / B 值创建颜色。 + * Creates a color with the given R/G/B values. + */ + constructor(r: number, g: number, b: number); + + /** + * 在 this 和 o 之间线性插值。 + * Linear interpolation between this and o by ratio n. + */ + lerp(o: GameRGBColor, n: number): GameRGBColor; + + /** + * 生成一个随机 RGB 颜色 (每个通道 0‑1)。 + * Generates a random RGB color (each channel 0–1). + */ + static random(): GameRGBColor; + + toString(): string; +} + +// ────────────────────────────────────────────── + +/** + * RGBA 颜色 (四个通道, 每通道 0.0‑1.0)。 + * An RGBA color; all four channels range from 0.0 to 1.0. + */ +declare class GameRGBAColor { + /** 红色通道 (0.0‑1.0)。Red channel. */ + r: number; + /** 绿色通道 (0.0‑1.0)。Green channel. */ + g: number; + /** 蓝色通道 (0.0‑1.0)。Blue channel. */ + b: number; + /** Alpha (不透明度), 范围 0.0–1.0。Alpha (opacity), range 0.0–1.0. */ + a: number; + + constructor(r: number, g: number, b: number, a: number); + + /** 原地设置所有四个通道。Sets all four channels in‑place. */ + set(r: number, g: number, b: number, a: number): GameRGBAColor; + + /** 原地复制另一个颜色的值。Copies values from another RGBA color in‑place. */ + copy(c: GameRGBAColor): GameRGBAColor; + + /** 深拷贝。Returns a new independent copy. */ + clone(): GameRGBAColor; + + /** 逐通道加法 (返回新对象)。Channel‑wise addition (returns new object). */ + add(rgba: GameRGBAColor): GameRGBAColor; + + /** 逐通道减法 (返回新对象)。Channel‑wise subtraction (returns new object). */ + sub(rgba: GameRGBAColor): GameRGBAColor; + + /** 逐通道乘法 (返回新对象)。Channel‑wise multiplication (returns new object). */ + mul(rgba: GameRGBAColor): GameRGBAColor; + + /** 逐通道除法 (返回新对象, 除以 0 得 0)。Channel‑wise division (returns new object; divide‑by‑zero → 0). */ + div(rgba: GameRGBAColor): GameRGBAColor; + + /** 原地加法。Addition in‑place. */ + addEq(rgba: GameRGBAColor): GameRGBAColor; + + /** 原地减法。Subtraction in‑place. */ + subEq(rgba: GameRGBAColor): GameRGBAColor; + + /** 原地乘法。Multiplication in‑place. */ + mulEq(rgba: GameRGBAColor): GameRGBAColor; + + /** 原地除法 (除以 0 跳过该通道)。Division in‑place (divide‑by‑zero skips that channel). */ + divEq(rgba: GameRGBAColor): GameRGBAColor; + + /** 线性插值。Linear interpolation between this and rgba by ratio n. */ + lerp(rgba: GameRGBAColor, n: number): GameRGBAColor; + + /** 近似相等检查 (容差 1e‑6)。Approximate equality within 1e‑6 tolerance. */ + equals(rgba: GameRGBAColor): boolean; + + /** + * Alpha 混合: 将自身 RGBA 颜色混合到 RGB 背景上。 + * Blends this RGBA color onto an RGB background, returning the displayed RGB. + */ + blendEq(rgb: GameRGBColor): GameRGBColor; + + toString(): string; +} + +// ────────────────────────────────────────────── + +/** + * 四元数, 用于三维旋转。 + * A quaternion used for 3‑dimensional rotation. + * + * @remarks + * 单位四元数 (w²+x²+y²+z²=1) 表示纯旋转。 + * Unit quaternions represent pure rotations. + */ +declare class GameQuaternion { + /** 实部 (标量分量)。Real (scalar) component. */ + w: number; + /** 虚部 X 分量。Imaginary X component. */ + x: number; + /** 虚部 Y 分量。Imaginary Y component. */ + y: number; + /** 虚部 Z 分量。Imaginary Z component. */ + z: number; + + /** 创建单位四元数 (1, 0, 0, 0)。Creates an identity quaternion. */ + constructor(); + + /** 用指定的 w/x/y/z 分量创建四元数。 */ + constructor(w: number, x: number, y: number, z: number); + + /** 原地设置所有分量。Sets all components in‑place. */ + set(w: number, x: number, y: number, z: number): GameQuaternion; + + /** 原地复制。Copies values from another quaternion in‑place. */ + copy(v: GameQuaternion): GameQuaternion; + + /** 深拷贝。Returns a new independent copy. */ + clone(): GameQuaternion; + + /** 逐分量加法。Component‑wise addition. */ + add(v: GameQuaternion): GameQuaternion; + + /** 逐分量减法。Component‑wise subtraction. */ + sub(v: GameQuaternion): GameQuaternion; + + /** + * 四元数乘法 (汉密尔顿积): this × q。 + * Hamilton product: this × q. + * @remarks 注意乘法不满足交换律。Multiplication is NOT commutative. + */ + mul(q: GameQuaternion): GameQuaternion; + + /** + * 共轭四元数 (对单位四元数等价于逆)。 + * Conjugate of this quaternion (equals inverse for unit quaternions). + */ + inv(): GameQuaternion; + + /** 除法: this × q⁻¹。Division: this × q⁻¹. */ + div(q: GameQuaternion): GameQuaternion; + + /** 点积: this · q。Dot product. */ + dot(q: GameQuaternion): number; + + /** 模长 (范数)。Magnitude (norm). */ + mag(): number; + + /** 模长平方。Squared magnitude. */ + sqrMag(): number; + + /** + * 单位化: 返回模长为 1 的新四元数。 + * Normalizes this quaternion; returns a unit quaternion. + */ + normalize(): GameQuaternion; + + /** + * 球面线性插值 (Slerp): 在 this 和 q 之间平滑旋转。 + * Spherical linear interpolation — smooth rotation between this and q. + * @param t - 插值比率 (0=this, 1=q) / interpolation factor + */ + slerp(q: GameQuaternion, t: number): GameQuaternion; + + /** + * 返回 this 和 q 之间的角度 (弧度)。 + * Angular difference between this and q (in radians). + */ + angle(q: GameQuaternion): number; + + /** + * 返回四元数对应的轴‑角表示。 + * Decomposes this quaternion into axis‑angle representation. + * @returns 包含 `angle` 和 `axis` 字段的对象 / object with `angle` and `axis` fields + */ + getAxisAngle(): AxisAngle; + + // ── 旋转操作 / Rotation operations ── + + /** 绕 X 轴旋转 (在左侧乘以旋转四元数)。Rotate around X axis. */ + rotateX(rad: number): GameQuaternion; + /** 绕 Y 轴旋转。Rotate around Y axis. */ + rotateY(rad: number): GameQuaternion; + /** 绕 Z 轴旋转。Rotate around Z axis. */ + rotateZ(rad: number): GameQuaternion; + + // ── 静态构造器 / Static constructors ── + + /** 从轴‑角表示创建四元数。Create from axis‑angle representation. */ + static fromAxisAngle(axis: GameVector3, rad: number): GameQuaternion; + + /** + * 从欧拉角创建四元数 (YZX 旋转顺序)。 + * Create from Euler angles (YZX rotation order: Y → Z → X). + */ + static fromEuler(x: number, y: number, z: number): GameQuaternion; + + /** + * 计算从向量 a 旋转到向量 b 的最短弧四元数。 + * Shortest‑arc quaternion rotating from vector a to vector b. + */ + static rotationBetween(a: GameVector3, b: GameVector3): GameQuaternion; + + /** 近似相等检查 (容差 1e‑6)。 */ + equals(v: GameQuaternion): boolean; + + toString(): string; +} + +/** + * 轴‑角表示的返回类型 (由 getAxisAngle() 返回)。 + * Return type for quaternion.getAxisAngle(). + */ +interface AxisAngle { + /** 旋转角度 (弧度) / rotation angle in radians */ + angle: number; + /** 旋转轴 (单位向量) / rotation axis (unit vector) */ + axis: GameVector3; +} + +// ================================================================ +// §2 Storage Types — 持久化存储 +// ================================================================ + +/** + * 数据存储空间 (键值持久化)。 + * A data‑storage namespace — persistent key‑value store backed by JSON files. + * + * @remarks + * 通过 `storage.getDataStorage("name")` 获取。 + * Obtain via `storage.getDataStorage("name")`. + */ +interface GameDataStorage { + /** + * 获取存储空间名称 (只读)。 + * @en Returns the read‑only namespace name. + */ + readonly key: string; + + /** + * 存入一个键值对。值必须是可 JSON 序列化的类型。 + * Stores a key‑value pair. Value must be JSON‑serializable. + * @param key - 键 / key + * @param value - 值 (number | string | boolean | object | array | null) / value + */ + set(key: string, value: unknown): void; + + /** + * 读取键对应的值, 不存在则返回 null。 + * Retrieves the value for a key, or null if it does not exist. + * @returns 存储的值, 或 null + */ + get(key: string): unknown; + + /** + * 获取当前存储空间中的所有键。 + * Lists all keys in this storage namespace. + */ + keys(): string[]; + + /** + * 原子更新: 取出当前值, 用 handler(currentValue) 的结果覆盖。 + * Atomically updates a value using a callback. + * @param key - 键 / key + * @param handler - (prevValue) => newValue / callback receiving the old value, returning the new one + * @remarks 如果键不存在, 不会创建新条目 (遵循 Box3 规范)。 + * If the key does not exist, nothing happens (per Box3 spec). + */ + update(key: string, handler: (prevValue: unknown) => unknown): void; + + /** + * 删除键, 返回旧值 (不存在则返回 null)。 + * Removes a key and returns its previous value, or null. + * @returns 被删除的旧值 / the previous value, or null + */ + remove(key: string): unknown; + + /** + * 原子递增 (delta 默认为 1)。 + * Atomically increments a numeric value by delta (default 1). + * @param key - 键 / key + * @param delta - 增量 (可选, 默认 1) / increment amount (optional, default 1) + * @returns 递增后的新值 / the new value after incrementing + * @remarks 键不存在时从 0 + delta 开始。 + * If the key doesn't exist, starts from 0 + delta. + */ + increment(key: string, delta?: number): number; + + /** + * 分页查询存储条目。 + * Paginated query of stored entries. + * @param options - 查询选项 / query options + * @param options.cursor - 起始游标 (页码) / starting cursor (page number * pageSize) + * @param options.pageSize - 每页条目数 (1‑100, 默认 100) / items per page (1–100, default 100) + * @param options.ascending - 是否升序排列 / sort ascending if true + * @param options.max - 值的上限过滤 / maximum value filter + * @param options.min - 值的下限过滤 / minimum value filter + * @param options.constraintTarget - 排序/过滤的目标路径 (如 "a.b.c") / nested path for sorting/filtering + * @returns 分页结果对象 / paginated query result + */ + list(options?: { + cursor?: number; + pageSize?: number; + ascending?: boolean; + max?: number; + min?: number; + constraintTarget?: string; + }): QueryList; + + /** + * 销毁该存储空间 (删除对应 JSON 文件)。 + * Destroys this storage namespace (deletes the backing JSON file). + */ + destroy(): void; +} + +/** + * 分页查询结果 (由 GameDataStorage.list() 返回)。 + * Paginated query result returned by GameDataStorage.list(). + */ +interface QueryList { + /** 是否已到达最后一页。Whether the last page has been reached. */ + isLastPage: boolean; + + /** + * 获取当前页的条目数组。 + * Returns the entries for the current page. + */ + getCurrentPage(): ReturnValue[]; + + /** + * 移动到下一页。 + * Advances the cursor to the next page. + */ + nextPage(): void; +} + +/** + * 单个存储条目 (包含元数据)。 + * A single stored entry with metadata. + */ +interface ReturnValue { + /** 键名 / key name */ + key: string; + /** 值 / stored value */ + value: unknown; + /** 更新时间 (Unix 毫秒) / last‑modified timestamp (Unix ms) */ + updateTime: number; + /** 创建时间 (Unix 毫秒) / creation timestamp (Unix ms) */ + createTime: number; + /** 版本标识符 (可用于乐观锁) / version identifier (usable for optimistic locking) */ + version: string; +} + +/** + * 全局存储入口 — 脚本中通过 `storage` 访问。 + * Global storage entry point — accessed via `storage` in scripts. + * + * @remarks + * 项目间数据隔离: 每个项目自动使用项目名作为存储文件前缀。 + * Per‑project isolation: each project's storage is automatically prefixed with the project name. + * 跨项目共享: `getGroupStorage` 使用 `__shared__/` 命名空间, 所有项目访问同一数据。 + * Cross‑project sharing: `getGroupStorage` uses a `__shared__/` namespace visible to all projects. + */ +interface GameStorage { + /** 始终返回空字符串 (MC 本地存储无 key)。Always returns "" for MC local storage. */ + key: string; + + /** + * 打开或创建指定名称的数据存储空间 (项目隔离)。 + * Opens or creates a named data‑storage namespace (per‑project isolated). + * @param name - 命名空间 (可含 "/" 作为目录分隔) / namespace (may contain "/" as directory separator) + * @remarks 不同项目使用同一 name 会访问不同文件。 + * Different projects using the same name access different files. + */ + getDataStorage(name: string): GameDataStorage; + + /** + * 获取跨项目共享存储 — 所有项目通过同一 name 读写同一份数据。 + * Shared cross‑project storage — all projects read/write the same data by name. + * @param name - 命名空间 / namespace + * @remarks 底层使用 `__shared__/` 前缀, 适合全服排行榜、全局配置等场景。 + * Uses `__shared__/` prefix internally; suitable for global leaderboards, shared config, etc. + */ + getGroupStorage(name: string): GameDataStorage; +} + +// ================================================================ +// §3 Entity — 实体 +// ================================================================ + +/** + * 实体包装, 可用于玩家或生物。 + * Entity wrapper — represents a player or mob in the world. + * + * @remarks + * 通过 `world.querySelector()`, `world.querySelectorAll()` 或事件回调获取。 + * Obtained via `world.querySelector()`, `world.querySelectorAll()`, or event callbacks. + */ +interface GameEntity { + // ── 身份 / Identity ── + + /** + * 实体 UUID (字符串格式)。 + * Entity UUID as a string (e.g. "550e8400-e29b-41d4-a716-446655440000"). + */ + id: string; + + /** + * 是否为玩家实体。返回 true 后 player 属性自动收窄为非 null。 + * True if this entity is a player. After a truthy check, `player` is narrowed to non-null. + */ + isPlayer(): this is GameEntity & { player: GamePlayer }; + + /** + * 实体类型标识符 (如 "minecraft:zombie")。 + * Entity type identifier (e.g. "minecraft:zombie"). + */ + entityType: string; + + // ── 位置 & 运动 / Position & Movement ── + + /** + * 当前坐标 (世界坐标)。 + * Current world‑space position. + */ + position: GameVector3; + + /** + * 当前速度 (运动向量)。 + * Current velocity (motion vector). + */ + velocity: GameVector3; + + /** + * 包围盒半尺寸 (x=宽/2, y=高/2, z=宽/2)。 + * Bounding‑box half‑extents (x=width/2, y=height/2, z=width/2). + */ + bounds: GameVector3; + + /** + * 是否在地面上。 + * True if the entity is standing on a block. + */ + onGround: boolean; + + /** + * 视线起始点 (眼部位置)。 + * Eye position (raycast origin for the entity's view). + */ + eyePosition: GameVector3; + + // ── 生命状态 / Lifecycle ── + + /** + * 当前生命值。 + * Current health (HP). + */ + hp: number; + + /** + * 最大生命值。 + * Maximum health. + */ + maxHp: number; + + /** + * 实体是否已被移除/销毁 (true = 已移除)。 + * Whether the entity has been removed / destroyed (true = removed). + */ + destroyed: boolean; + + /** + * 设置实体着火 tick 数 (0 = 灭火)。 + * Sets the remaining fire ticks (0 = extinguish). + */ + setFire(ticks: number): void; + + /** 灭火。Extinguishes any fire on the entity. */ + clearFire(): void; + + // ── 伤害 & 恢复 / Damage & Healing ── + + /** + * 对实体造成伤害。 + * Deals generic damage to the entity. + * @param amount - 伤害值 (半心) / damage amount in half‑hearts + */ + hurt(amount: number): void; + + /** + * 治疗实体。 + * Heals the entity. + * @param amount - 治疗量 (半心) / healing amount in half‑hearts + */ + heal(amount: number): void; + + // ── 外观 / Appearance ── + + /** + * 是否不可见 (隐身)。 + * True if the entity is invisible. + */ + meshInvisible: boolean; + + /** 是否发光 (轮廓高亮)。Whether glow outline is active. */ + glowing: boolean; + + /** + * 名称标签文本 (空字符串 = 无)。 + * Custom name tag text (empty string = none). + */ + nameTag: string; + setNameTag(name: string): void; + + // ── 无敌 & 持久化 / Invulnerability & Persistence ── + + /** 是否无敌。Whether the entity is invulnerable to damage. */ + invulnerable: boolean; + + /** + * 设置为持久化实体 (防止被自然清除)。 + * Marks the entity as persistent (prevents it from being despawned naturally). + * @remarks 仅写方法, 无 getter。Write‑only method, no getter available. + */ + setPersistent(v: boolean): void; + + // ── 标签 / Tags ── + + /** 添加一个标签。Adds a scoreboard tag. */ + addTag(tag: string): void; + + /** 移除一个标签。Removes a scoreboard tag. */ + removeTag(tag: string): void; + + /** 检查是否拥有指定标签。Checks whether the entity has the given tag. */ + hasTag(tag: string): boolean; + + // ── 效果 / Effects ── + + /** + * 添加状态效果。 + * Applies a status effect to the entity. + * @param effectId - 效果 ID (如 "minecraft:speed") + * @param duration - 持续时间 (tick) + * @param amplifier - 等级 (0 = 一级) + * @param hideParticles - 是否隐藏粒子 (可选, 默认 false) + */ + addEffect( + effectId: string, + duration: number, + amplifier: number, + hideParticles?: boolean, + ): void; + + // ── 属性 / Attributes ── + + /** + * 读取实体属性值。 + * Reads a registered entity attribute value. + * @param attributeId - 属性 ID (如 "minecraft:generic.max_health") + * @returns 当前属性值, 不支持的实体返回 0 + */ + getAttribute(attributeId: string): number; + + /** + * 设置实体属性基础值。 + * Sets the base value of a registered entity attribute. + * @param attributeId - 属性 ID (如 "minecraft:generic.movement_speed") + * @param value - 新基础值 / new base value + * @remarks 仅对 LivingEntity 有效。Only works on living entities. + */ + setAttribute(attributeId: string, value: number): void; + + // ── 装备 / Equipment ── + + /** + * 给生物设置装备。 + * Equips an item onto a mob's equipment slot. + * @param slot - 槽位名称 / slot name: + * "mainhand", "offhand", "head"/"helmet"/"helm", + * "chest"/"chestplate", "legs"/"leggings", "feet"/"boots" + * @param itemId - 物品 ID (如 "minecraft:diamond_sword") + */ + setEquipment(slot: string, itemId: string): void; + + /** + * 设置装备掉落概率。 + * Sets the drop chance for an equipment slot. + * @param slot - 槽位名称 或 "all" / slot name or "all" for every slot + * @param chance - 掉落概率 (0‑1) / drop chance (0–1) + */ + setDropChance(slot: string, chance: number): void; + + // ── 导航 & AI / Navigation & AI ── + + /** + * 让生物导航到指定坐标。 + * Orders a pathfinder mob to navigate to the given coordinates. + * @param x, y, z - 目标坐标 + * @param speed - 移动速度倍率 + * @returns 路径计算成功返回 true, 非 PathfinderMob 返回 false + */ + navigateTo(x: number, y: number, z: number, speed: number): boolean; + /** GameVector3 重载。GameVector3 overload. */ + navigateTo(pos: GameVector3, speed: number): boolean; + + /** + * 设置生物的当前攻击目标。 + * Sets the mob's attack target (the mob will pathfind to and attack it). + */ + setTarget(target: GameEntity): void; + + /** 清除攻击目标, 停止追击。Clears the attack target, stopping pursuit. */ + clearTarget(): void; + + /** + * 获取当前攻击目标 (可能为 null)。 + * Returns the mob's current attack target, or null. + */ + getTarget(): GameEntity | null; + + /** + * 启用或禁用生物 AI (寻路/目标等)。 + * Enables or disables the mob's AI (pathfinding, goals, etc.). + */ + setAI(enabled: boolean): void; + + // ── 朝向 / Look direction ── + + /** + * 让实体看向指定坐标。 + * Makes the entity look at a point in space. + */ + lookAt(x: number, y: number, z: number): void; + lookAt(pos: GameVector3): void; + + // ── 生命周期 / Lifecycle ── + + /** + * 销毁实体 (触发 onDestroy 回调)。 + * Destroys the entity (triggers any registered onDestroy callback). + */ + destroy(): void; + + /** + * 移除实体 (不触发 onDestroy 回调)。 + * Removes the entity WITHOUT triggering onDestroy callback. + */ + remove(): void; + + /** + * 注册实体被销毁时的回调。 + * Registers a callback to be called when this entity is destroyed. + */ + setOnDestroy(handler: (entity: GameEntity) => void): void; + + // ── 玩家代理 / Player proxy ── + + /** + * 玩家接口 (仅当 isPlayer 为 true 时非 null)。 + * The player interface — non‑null only when isPlayer is true. + */ + player: GamePlayer | null; +} + +// ================================================================ +// §4 Player — 玩家 +// ================================================================ + +/** + * 玩家扩展接口 (通过 entity.player 访问)。 + * Player‑specific interface — accessed via `entity.player`. + */ +interface GamePlayer { + // ── 身份 / Identity ── + + /** 玩家名。Player display name. */ + name: string; + /** 玩家 UUID (与 entity.id 相同)。Player UUID (same as entity.id). */ + userId: string; + + // ── 外观 / Appearance ── + + /** + * 是否隐身。 + * Whether the player is invisible. + */ + invisible: boolean; + + /** + * 模型缩放比例 (MC 原生, 非 Box3 scale)。 + * Player model scale (Minecraft native, not Box3 scale). + */ + readonly scale: number; + + // ── 移动 / Movement ── + + /** 行走速度 (基础值)。Walk speed (base attribute value). */ + walkSpeed: number; + + /** + * 疾跑速度 (≈ walkSpeed × 1.3)。 + * Run/sprint speed (≈ walkSpeed × 1.3). + */ + runSpeed: number; + + /** + * 跳跃力度。 + * Jump power (jump strength attribute). + */ + jumpPower: number; + + /** + * 当前移动状态。 + * Current movement state. + * @returns "FLYING" | "GROUND" | "SWIM" | "FALL" | "JUMP" + */ + readonly moveState: string; + + /** + * 当前行走状态。 + * Current walk state. + * @returns "NONE" | "CROUCH" | "WALK" | "RUN" + */ + readonly walkState: string; + + // ── 飞行 & 碰撞 / Flying & Collision ── + + /** 是否允许飞行。Whether flight is enabled. */ + canFly: boolean; + + /** 是否正在飞行。Whether the player is currently flying. */ + flying: boolean; + + /** 飞行速度。Flying speed. */ + flySpeed: number; + + /** + * 碰撞开关 (通过队伍碰撞规则实现)。 + * Collision toggle (implemented via team collision rules). + */ + collision: boolean; + + /** 是否为观察者模式。Whether the player is in spectator mode. */ + readonly spectator: boolean; + + /** 是否禁用飞行 (不允许且自动关闭飞行)。Whether flying is disabled entirely. */ + disableFly: boolean; + + // ── 游戏模式 / Game Mode ── + + /** + * 游戏模式字符串 (如 "survival", "creative", "adventure", "spectator")。 + * Game mode as a string (e.g. "survival", "creative", "adventure", "spectator"). + * 也可以接受数字 (0=survival, 1=creative, 2=adventure, 3=spectator)。 + */ + gameMode: string | number; + + /** + * 当前维度 ID (如 "minecraft:overworld")。 + * Current dimension identifier. + */ + dimension: string; + + // ── 相机 / Camera ── + + /** + * 相机模式。 + * Camera mode. + * @default "FPS" + */ + cameraMode: string; + + /** + * 相机跟随的实体 (在 FOLLOW 模式下)。 + * The entity the camera follows (when in FOLLOW mode). + */ + cameraEntity: GameEntity | null; + + /** 相机俯仰角。Camera pitch (vertical rotation). */ + cameraPitch: number; + + /** 相机偏航角。Camera yaw (horizontal rotation). */ + cameraYaw: number; + + /** + * 玩家面朝方向 (单位向量)。 + * Direction the player is facing (unit vector). + */ + readonly facingDirection: GameVector3; + + /** + * 玩家视线前方 5 格处的目标点。 + * A point 5 blocks ahead of the player's eyes (look‑at target). + */ + readonly cameraTarget: GameVector3; + + // ── 生命 / Vital stats ── + + /** 饥饿值 (0‑20)。Food level (0–20). */ + food: number; + + /** 饱和度 (0‑20)。Saturation level (0–20). */ + saturation: number; + + /** 当前生命值。Current health. */ + hp: number; + /** 最大生命值。Maximum health. */ + maxHp: number; + + // ── 经验 / Experience ── + + /** 经验等级 (与 /xp 命令相同)。Experience level (same as /xp command). */ + xp: number; + + /** 增加经验等级。Adds experience levels to the player. */ + addExperienceLevels(levels: number): void; + + // ── 传送 / Teleport ── + + /** + * 将玩家传送到指定坐标。 + * Teleports the player to the given coordinates. + */ + teleport(pos: GameVector3): void; + + // ── 重生 / Respawn ── + + /** + * 设置重生点。 + * Sets the player's respawn point. + */ + setRespawnPoint(pos: GameVector3): void; + + /** + * 强制重生 (仅在死亡状态下有效)。 + * Forces a respawn (only works when dead). + */ + respawn(): void; + + // ── 踢出 / Kick ── + + /** 踢出玩家 (默认理由 "Kicked")。Kicks the player with default reason. */ + kick(): void; + /** 踢出玩家 (自定义理由)。Kicks the player with a custom reason. */ + kick(reason: string): void; + + // ── 消息 / Messaging ── + + /** + * 发送仅该玩家可见的聊天消息。 + * Sends a chat message visible only to this player. + */ + directMessage(msg: string): void; + + /** + * 在动作栏 (快捷栏上方) 显示文字。 + * Displays text in the action bar (above the hotbar). + */ + actionBar(message: string): void; + + /** + * 显示屏幕标题。 + * Displays a screen title. + * @param title - 主标题 + * @param subtitle - 副标题 + * @param fadeIn - 淡入 tick (可选, 默认 10) + * @param stay - 停留 tick (可选, 默认 70) + * @param fadeOut - 淡出 tick (可选, 默认 20) + */ + title( + title: string, + subtitle: string, + fadeIn?: number, + stay?: number, + fadeOut?: number, + ): void; + + /** + * 弹出对话面板 (简化版, MC 目前仅发送文本)。 + * Shows a dialog panel — simplified; currently just sends text in MC. + * @param config.content - 对话内容 + * @param config.options - 选项数组 + * @returns 用户选择结果 { index, value } + */ + dialog(config: { content?: string; options?: string[] }): { + index: number; + value: string; + }; + + // ── 链接 / Link ── + + /** + * 向玩家发送可点击的 URL 链接。 + * Sends a clickable URL link to the player. + */ + link(href: string): void; + + // ── 计分板名称 / Tab list name ── + + /** + * 设置玩家在 TAB 列表中的显示名称 (支持颜色代码)。 + * Sets the player's display name in the tab list (supports color codes). + */ + setPlayerListName(name: string): void; + + // ── 朝向 / Look direction ── + + /** + * 让玩家看向指定坐标。 + * Makes the player look at a point in space. + */ + lookAt(x: number, y: number, z: number): void; + lookAt(pos: GameVector3): void; + + // ── 执行命令 / Command ── + + /** + * 以玩家身份执行 Minecraft 命令。 + * Executes a Minecraft command as this player. + */ + runCommand(cmd: string): void; + + // ── 物品栏 / Inventory ── + + /** + * 给予玩家物品。 + * Gives an item to the player. + * @param itemId - 物品 ID (如 "minecraft:diamond") + * @param count - 数量 (1‑64) + */ + giveItem(itemId: string, count: number): void; + + /** + * 给予玩家附魔物品。 + * Gives an enchanted item to the player. + * @param itemId - 物品 ID + * @param count - 数量 + * @param enchants - 附魔对象 (如 { "minecraft:sharpness": 5 }) + */ + giveEnchantedItem( + itemId: string, + count: number, + enchants: Record, + ): void; + + /** + * 给予玩家带自定义名称和描述的命名物品。 + * Gives an item with a custom name and lore. + * @param itemId - 物品 ID + * @param count - 数量 + * @param customName - 自定义名称 + * @param lore - 描述文字数组 + */ + giveNamedItem( + itemId: string, + count: number, + customName: string, + lore: string[], + ): void; + + /** + * 获取手持物品信息。 + * Returns info about the currently held item. + * @returns { id: string, count: number } + */ + getHeldItem(): { id: string; count: number }; + + /** 清空背包。Clears the player's inventory. */ + clearInventory(): void; + + /** 管理员权限等级 (0-4)。0=普通玩家, 4=最高权限。Server operator permission level (0–4). */ + opLevel: number; + + /** 管理员权限等级 (0-4)。0=普通玩家, 4=最高权限。Server operator permission level (0–4). */ + getOpLevel(): number; + + // ── 效果 / Effects ── + + /** + * 添加状态效果。 + * Applies a status effect. + * @param effectId - 效果 ID (如 "minecraft:speed") + * @param duration - 持续时间 (tick) + * @param amplifier - 等级 (0 = 一级) + * @param hideParticles - 是否隐藏粒子 (可选, 默认 false) + */ + addEffect( + effectId: string, + duration: number, + amplifier: number, + hideParticles?: boolean, + ): void; + + /** 清除所有状态效果。Removes all status effects. */ + clearEffects(): void; + + // ── 声音 / Sound ── + + /** + * 向该玩家播放声音。 + * Plays a sound for this player only. + * @param path - 声音 ID (如 "minecraft:block.note_block.pling") + * @param volume - 音量 (0‑1) + * @param pitch - 音高 (0.5‑2) + */ + playSound(path: string, volume: number, pitch: number): void; + + // ── 聊天 / Chat ── + + /** + * 为该玩家注册聊天处理器 (覆盖全局 onChat)。 + * Registers a per‑player chat handler (overrides global onChat for this player). + */ + onChat( + handler: (entity: GameEntity, message: string, tick: number) => void, + ): void; +} + +// ================================================================ +// §5 World — 世界 API +// ================================================================ + +/** + * 世界控制与事件 — 脚本中通过 `world` 访问。 + * World control & events — accessed via `world` in scripts. + */ +interface GameWorld { + // ── 世界属性 / World properties ── + + /** 服务器 MOTD。Server MOTD string. */ + projectName(): string; + + /** 当前服务端 tick 计数。Current server tick count. */ + currentTick(): number; + + /** + * 降雨强度 (0‑1)。 + * Rain density (0–1). + */ + rainDensity: number; + + /** + * 雷暴强度 (0‑1)。 + * Thunder density (0–1). + */ + thunderDensity: number; + + /** 清除天气 (晴天)。Clears weather to clear skies. */ + clearWeather(): void; + + // ── 时间 / Time ── + + /** + * 当前游戏内时间 (tick, 0‑24000)。 + * Current in‑game time in ticks (0–24000). + */ + time: number; + + /** + * 时间流速 (1=正常, 0=停止)。 + * Time scale (1 = normal, 0 = frozen). + */ + timeScale: number; + + /** + * 设置游戏内时间 (tick, 0‑24000)。 + * Sets the in-game time in ticks. + * @param time - 0=黎明, 6000=正午, 12000=黄昏, 18000=午夜 + */ + setTime(time: number): void; + + // ── 难度 / Difficulty ── + + /** + * 当前难度。 + * Current difficulty ("peaceful" | "easy" | "normal" | "hard"). + */ + difficulty: string; + + // ── 出生点 / Spawn ── + + /** + * 世界出生点坐标。 + * World spawn point coordinates. + */ + readonly spawnPoint: GameVector3; + + /** + * 设置世界出生点。 + * Sets the world spawn point. + */ + setWorldSpawn(pos: GameVector3): void; + + // ── 游戏规则 (MC 扩展) / Game Rules (MC extension) ── + + /** + * 读取游戏规则。 + * Reads a game‑rule value. + * @param name - 规则名 / rule name (see setGameRule for the list) + */ + getGameRule(name: string): boolean | null; + + /** + * 设置游戏规则。 + * Sets a game rule. + * @param name - supported: doDaylightCycle | doWeatherCycle | keepInventory | + * doMobSpawning | doFireTick | mobGriefing | doImmediateRespawn + * @param value - boolean or string "true"/"false" + */ + setGameRule(name: string, value: boolean | string): void; + + // ── 消息 & 声音 / Broadcasting ── + + /** + * 向全服广播消息。 + * Sends a chat message to all players. + */ + say(message: string): void; + + /** + * 在指定位置向全服播放声音。 + * Plays a sound for all players at a location. + * @param path - 声音 ID + * @param x, y, z - 声源坐标 + * @param volume - 音量 (0‑1) + * @param pitch - 音高 (0.5‑2) + */ + playSound( + path: string, + x: number, + y: number, + z: number, + volume: number, + pitch: number, + ): void; + playSound( + path: string, + pos: GameVector3, + volume: number, + pitch: number, + ): void; + + // ── 命令 / Command ── + + /** + * 以服务端身份执行命令。 + * Executes a Minecraft command as the server. + */ + runCommand(cmd: string): void; + + // ── 实体查询 / Entity Queries ── + + /** + * 查询所有匹配选择器的实体 (目前仅限玩家)。 + * Selects all entities matching a selector (currently only players). + * @param selector - "*" (所有玩家) | "#uuid" | ".tag" + */ + querySelectorAll(selector: string): GameEntity[]; + + /** + * 查询第一个匹配的实体 (或 null)。 + * Selects the first matching entity, or null. + */ + querySelector(selector: string): GameEntity | null; + + /** + * 查询指定区域内的所有实体。 + * Returns all entities inside an AABB defined by two corners. + */ + entitiesInArea(pos1: GameVector3, pos2: GameVector3): GameEntity[]; + + /** + * 查询指定半径内的所有实体。 + * Returns all entities within a radius around a point. + */ + entitiesInRadius(x: number, y: number, z: number, radius: number): GameEntity[]; + entitiesInRadius(pos: GameVector3, radius: number): GameEntity[]; + + // ── 实体生成 / Entity Spawning ── + + /** + * 在指定位置生成实体。 + * Spawns an entity at the given position. + * @param type - 实体类型 ID (如 "minecraft:zombie") + * @param pos - 生成坐标 + * @returns 生成的实体包装, 失败返回 null + */ + spawnEntity(type: string, pos: GameVector3): GameEntity | null; + + // ── 射线检测 / Raycast ── + + /** + * 从起点向指定方向发射射线, 返回碰撞结果。 + * Casts a ray and returns hit information. + * @param origin - 起点 + * @param direction - 方向向量 (自动归一化) + * @param maxDistance - 最大距离 (可选, 默认 5) + * @returns { hit, x, y, z, normalX, normalY, normalZ, distance, entity?, voxel? } + */ + raycast( + origin: GameVector3, + direction: GameVector3, + maxDistance?: number, + ): RaycastResult; + + // ── 生物群系 / Biome ── + + /** + * 获取指定位置的生物群系 ID。 + * Returns the biome identifier at the given position. + */ + getBiome(x: number, y: number, z: number): string; + getBiome(pos: GameVector3): string; + + // ── 爆炸 / Explosion ── + + /** + * 在指定位置制造爆炸。 + * Creates an explosion at the given position. + * @param x, y, z - 爆炸中心 + * @param power - 爆炸强度 + * @param fire - 是否产生火焰 (可选, 默认 false) + */ + explode(x: number, y: number, z: number, power: number, fire?: boolean): void; + explode(pos: GameVector3, power: number, fire?: boolean): void; + + // ── 粒子 / Particles ── + + /** + * 在指定位置生成粒子。 + * Spawns particles at a given location. + * @param type - 粒子 ID (如 "minecraft:flame") + * @param x, y, z - 位置 + * @param count - 数量 + * @param dx - X 扩散范围 + * @param dy - Y 扩散范围 + * @param dz - Z 扩散范围 + * @param speed - 粒子速度 + */ + spawnParticle( + type: string, + x: number, + y: number, + z: number, + count: number, + dx: number, + dy: number, + dz: number, + speed: number, + ): void; + spawnParticle( + type: string, + pos: GameVector3, + count: number, + dx: number, + dy: number, + dz: number, + speed: number, + ): void; + + /** + * 在指定圆环上生成粒子。 + * Spawns particles in a circle. + * @param x, y, z - 圆心 + * @param radius - 半径 + * @param type - 粒子 ID + * @param count - 数量 + */ + spawnParticleCircle( + x: number, + y: number, + z: number, + radius: number, + type: string, + count: number, + ): void; + spawnParticleCircle( + pos: GameVector3, + radius: number, + type: string, + count: number, + ): void; + + // ── 烟花 / Fireworks ── + + /** + * 在指定位置发射烟花。 + * Launches a firework rocket. + * @param x, y, z - 发射位置 + * @param color - 颜色名称: "red" | "blue" | "green" | "yellow" | "gold" | "white" | "aqua" | "pink" | "purple" + * @param shape - 形状: "ball" | "large_ball" | "star" | "creeper" | "burst" + */ + launchFirework( + x: number, + y: number, + z: number, + color: string, + shape: string, + ): void; + launchFirework(pos: GameVector3, color: string, shape: string): void; + + // ── 闪电 / Lightning ── + + /** + * 在指定位置召唤闪电。 + * Summons a lightning bolt at the given position. + * @param x, y, z - 位置 + * @param damage - 伤害值 (可选, 仅对实体造成) + * @returns 是否成功 + */ + strikeLightning(x: number, y: number, z: number, damage?: number): boolean; + strikeLightning(pos: GameVector3, damage?: number): boolean; + + // ── 掉落物 / Drop Item ── + + /** + * 在指定位置生成掉落物。 + * Drops an item stack at the given position. + * @param x, y, z - 位置 + * @param itemId - 物品 ID + * @param count - 数量 + */ + dropItem( + x: number, + y: number, + z: number, + itemId: string, + count: number, + ): void; + dropItem(pos: GameVector3, itemId: string, count: number): void; + + // ── 弹射物 / Projectile ── + + /** + * 从起点向目标发射弹射物。 + * Launches a projectile from origin toward a target. + * @param type - 弹射物类型 (如 "minecraft:arrow") + * @param x, y, z - 发射位置 + * @param tx, ty, tz - 目标位置 + * @param speed - 速度 + * @returns 弹射物实体, 失败返回 null + */ + launchProjectile( + type: string, + x: number, + y: number, + z: number, + tx: number, + ty: number, + tz: number, + speed: number, + ): GameEntity | null; + launchProjectile( + type: string, + pos: GameVector3, + target: GameVector3, + speed: number, + ): GameEntity | null; + + // ── 计分板 / Scoreboard ── + + /** + * 添加计分板目标 (默认 dummy 标准)。 + * Adds a scoreboard objective (default dummy criteria). + */ + addScoreboard(name: string): void; + + /** + * 添加计分板目标 (自定义标准)。 + * Adds a scoreboard objective with a custom criteria. + */ + addScoreboard(name: string, criteria: string): void; + + /** 移除计分板目标。Removes a scoreboard objective. */ + removeScoreboard(name: string): void; + + /** + * 设置实体/名称的分数。 + * Sets the score of an entity or name for a given objective. + */ + setScore( + entityOrName: string | GameEntity, + objectiveName: string, + value: number, + ): void; + + /** + * 获取分数。 + * Gets the score of an entity or name for a given objective. + */ + getScore(entityOrName: string | GameEntity, objectiveName: string): number; + + /** + * 在指定显示位置展示计分板。 + * Displays a scoreboard objective in a display slot. + * @param slot - "sidebar" | "list" | "belowname" + */ + showScoreboard(slot: string, objectiveName: string): void; + + /** + * 从显示位置隐藏计分板。 + * Hides a scoreboard from a display slot. + */ + hideScoreboard(slot: string): void; + + /** + * 列出计分板上所有玩家的分数。 + * Lists all player scores for a given objective. + * @returns Array<{ name: string, value: number }> + */ + listScores(objectiveName: string): Array<{ name: string; value: number }>; + + // ── Boss 血条 / Boss Bar ── + + /** + * 显示或更新 Boss 血条。 + * Shows or updates a boss bar. + * @param name - 血条 ID + * @param text - 显示文字 + * @param progress - 进度 (0‑1) + * @param color - 颜色: "red" | "blue" | "green" | "yellow" | "purple" | "pink" | "white" + */ + showBossbar( + name: string, + text: string, + progress: number, + color: string, + ): void; + + /** 移除 Boss 血条。Removes a boss bar by ID. */ + removeBossbar(name: string): void; + + // ── 队伍 / Teams ── + + /** + * 创建一个队伍。 + * Creates a scoreboard team. + * @param name - 队伍名 + * @param color - 颜色 (如 "aqua", "red", "blue" 等) + */ + createTeam(name: string, color: string): void; + + /** 删除队伍。Removes a team. */ + removeTeam(name: string): void; + + /** + * 将实体/名称加入队伍。 + * Adds an entity or name to a team. + */ + joinTeam(entityOrName: string | GameEntity, teamName: string): void; + + /** + * 将实体/名称移出队伍。 + * Removes an entity or name from its current team. + */ + leaveTeam(entityOrName: string | GameEntity): void; + + /** + * 获取实体/名称所在的队伍名 (不在任何队伍返回 null)。 + * Returns the team name of an entity or name, or null. + */ + getTeamOf(entityOrName: string | GameEntity): string | null; + + // ── 世界边界 / World Border ── + + /** 当前边界大小。Current world border size. */ + borderSize: number; + + /** + * 设置边界中心。 + * Sets the world border center. + */ + setBorderCenter(x: number, z: number): void; + + /** + * 缩放边界到目标大小 (带动画)。 + * Shrinks/grows the world border to a target size over time. + * @param targetSize - 目标大小 + * @param seconds - 动画秒数 + */ + shrinkBorder(targetSize: number, seconds: number): void; + + /** + * 边界伤害 (每秒造成的伤害值)。 + * World border damage per block per second. + */ + setBorderDamage(damage: number): void; + + /** + * 边界警告距离 (方块数)。 + * World border warning distance in blocks. + */ + setBorderWarning(blocks: number): void; + + // ── 定时器 / Timers ── + + /** + * 设置一次性延时回调。 + * Schedules a one‑shot delayed callback. + * @param handler - 回调函数 + * @param ticks - 延迟 tick 数 + * @returns 定时器 ID (可用于 clearTimeout) + */ + setTimeout(handler: () => void, ticks: number): number; + + /** + * 设置循环定时回调。 + * Schedules a recurring interval callback. + * @param handler - 回调函数 + * @param ticks - 间隔 tick 数 + * @returns 定时器 ID (可用于 clearInterval) + */ + setInterval(handler: () => void, ticks: number): number; + + /** 取消 setTimeout。Clears a timeout by ID. */ + clearTimeout(id: number): void; + + /** 取消 setInterval。Clears an interval by ID. */ + clearInterval(id: number): void; + + // ── 项目间消息 / Cross‑project Messaging ── + + /** + * 向另一个项目发送消息。 + * Sends a message to another script project. + * @param target - 目标项目名 (不含路径) + * @param data - 数据 (任意 JSON 可序列化的值) + */ + sendMessage(target: string, data: unknown): void; + + // ═══════════════════════════════════════════════════ + // 事件注册 / Event Registration + // ═══════════════════════════════════════════════════ + + /** + * 注册每 tick 回调 (每秒 20 次)。 + * Registers a callback invoked every tick (20 times/sec). + */ + onTick(handler: () => void): void; + + /** + * 注册玩家加入回调。 + * Registers a callback invoked when a player joins the server. + */ + onPlayerJoin(handler: (entity: GameEntity) => void): void; + + /** + * 注册玩家离开回调。 + * Registers a callback invoked when a player leaves the server. + */ + onPlayerLeave(handler: (entity: GameEntity) => void): void; + + /** + * 注册聊天消息回调 (包括 /me 消息)。 + * Registers a callback for chat messages (including /me). + * @param handler - (entity, message, tick) => void + */ + onChat( + handler: (entity: GameEntity, message: string, tick: number) => void, + ): void; + + /** + * 注册玩家重生回调。 + * Registers a callback invoked when a player respawns. + */ + onPlayerRespawn(handler: (entity: GameEntity) => void): void; + + /** + * 注册方块右键激活回调。 + * Registers a callback invoked when a player right‑clicks a block. + */ + onBlockActivate( + handler: ( + entity: GameEntity, + x: number, + y: number, + z: number, + voxel: string, + tick: number, + ) => void, + ): void; + + /** + * 注册方块破坏回调。 + * Registers a callback invoked when a player breaks a block. + */ + onVoxelDestroy( + handler: ( + entity: GameEntity, + x: number, + y: number, + z: number, + voxel: string, + tick: number, + ) => void, + ): void; + + /** + * 注册方块放置回调。 + * Registers a callback invoked when a player places a block. + */ + onBlockPlace( + handler: ( + entity: GameEntity, + x: number, + y: number, + z: number, + voxel: string, + voxelId: number, + tick: number, + ) => void, + ): void; + + /** + * 注册方块接触回调 (玩家移动到新方块时触发)。 + * Registers a callback invoked when a player's block position changes. + */ + onVoxelContact( + handler: ( + entity: GameEntity, + voxelId: number, + x: number, + y: number, + z: number, + contactType: number, + force: number, + tick: number, + ) => void, + ): void; + + /** + * 注册实体交互回调 (玩家右键实体)。 + * Registers a callback invoked when a player right‑clicks an entity. + */ + onInteract( + handler: (entity: GameEntity, target: GameEntity, tick: number) => void, + ): void; + + /** + * 注册实体死亡回调。 + * Registers a callback invoked when an entity dies. + */ + onEntityDeath( + handler: (entity: GameEntity, killer: GameEntity | null, tick: number) => void, + ): void; + + /** + * 注册实体受伤回调。 + * Registers a callback invoked when an entity takes damage. + */ + onEntityDamage( + handler: ( + entity: GameEntity, + amount: number, + source: string, + attacker: GameEntity | null, + tick: number, + ) => void, + ): void; + + /** + * 注册流体进入回调 (玩家进入水/熔岩)。 + * Registers a callback invoked when a player enters a fluid. + */ + onFluidEnter( + handler: ( + entity: GameEntity, + fluid: string, + x: number, + y: number, + z: number, + tick: number, + ) => void, + ): void; + + /** + * 注册流体离开回调 (玩家离开水/熔岩)。 + * Registers a callback invoked when a player leaves a fluid. + */ + onFluidLeave( + handler: ( + entity: GameEntity, + fluid: string, + x: number, + y: number, + z: number, + tick: number, + ) => void, + ): void; + + /** + * 注册实体接触回调 (两个实体碰撞)。 + * Registers a callback invoked when two entities come into contact. + */ + onEntityContact( + handler: (entityA: GameEntity, entityB: GameEntity, tick: number) => void, + ): void; + + /** + * 注册实体分离回调 (两个实体不再碰撞)。 + * Registers a callback invoked when two entities separate after contact. + */ + onEntitySeparate( + handler: (entityA: GameEntity, entityB: GameEntity, tick: number) => void, + ): void; + + /** + * 注册跨项目消息回调。 + * Registers a callback for messages from other script projects. + */ + onMessage(handler: (sender: string, data: unknown) => void): void; +} + +/** + * raycast() 返回结果。 + * Return type of world.raycast(). + */ +interface RaycastResult { + /** 是否命中。True if something was hit. */ + hit: boolean; + /** 命中点 X 坐标。Hit point X coordinate. */ + x: number; + /** 命中点 Y 坐标。Hit point Y coordinate. */ + y: number; + /** 命中点 Z 坐标。Hit point Z coordinate. */ + z: number; + /** 表面法线 X 分量。Surface normal X component. */ + normalX: number; + /** 表面法线 Y 分量。Surface normal Y component. */ + normalY: number; + /** 表面法线 Z 分量。Surface normal Z component. */ + normalZ: number; + /** 命中距离。Distance from origin to hit point. */ + distance: number; + /** 命中的方块 ID (命中方块时为数字)。Hit block ID (number when a block was hit). */ + voxel?: number; + /** 命中的实体 (命中实体时)。The entity that was hit (when an entity was hit). */ + entity?: GameEntity; +} + +// ================================================================ +// §6 Voxels — 方块操作 +// ================================================================ + +/** + * 方块读写操作 — 脚本中通过 `voxels` 访问。 + * Voxel (block) read/write — accessed via `voxels` in scripts. + * + * @remarks + * 所有坐标使用世界方块坐标 (整数)。 + * All coordinates are in world block space (integers). + */ +interface GameVoxels { + // ── 世界尺寸 / World dimensions ── + + /** + * 世界最大尺寸 (x, y, z 均为世界高度)。 + * Maximum world dimensions (x/y/z all equal world height). + */ + readonly shape: GameVector3; + + /** + * 所有可用的方块类型名称数组。 + * Array of all registered block type resource‑location strings. + */ + readonly VoxelTypes: string[]; + + // ── 名称 ↔ ID 映射 / Name–ID mapping ── + + /** + * 将方块名称转为数字 ID。 + * Resolves a block name (e.g. "stone" or "minecraft:stone") to its numeric ID. + * @returns 数字 ID, 未知方块的返回 0 (air) + */ + id(name: string): number; + + /** + * 将数字 ID 转为方块名称。 + * Resolves a numeric ID back to a block name string. + * @returns ResourceLocation 字符串, 未知 ID 返回 "air" + */ + name(id: number): string; + + // ── 读取 / Read ── + + /** + * 获取方块数字 ID (不含旋转信息的基础 ID)。 + * Returns the base numeric block ID at the given position (without rotation encoding). + * @returns 基础方块 ID, 空气返回 0 / base block ID, 0 for air + */ + getVoxel(x: number, y: number, z: number): number; + getVoxel(pos: GameVector3): number; + + /** + * 获取方块数字 ID (不含旋转信息的基础 ID)。 + * Returns the base numeric block ID (without rotation encoding). + */ + getVoxelId(x: number, y: number, z: number): number; + getVoxelId(pos: GameVector3): number; + + /** + * 获取方块名称 (与 getVoxel 相同, 兼容旧 API)。 + * Alias for getVoxel — kept for Box3 compatibility. + */ + getVoxelName(x: number, y: number, z: number): string; + getVoxelName(pos: GameVector3): string; + + /** + * 获取方块旋转值 (0‑3, 对应南/西/北/东)。 + * Returns the block rotation: 0=South, 1=West, 2=North, 3=East. + */ + getVoxelRotation(x: number, y: number, z: number): number; + getVoxelRotation(pos: GameVector3): number; + + // ── 写入 / Write ── + + /** + * 放置方块 (名称或 ID)。返回含旋转编码的完整 ID。 + * Places a block by name or ID. Returns the full encoded ID (baseId + rotation * 16384). + * @param voxel - 方块名称 (如 "minecraft:diamond_block") 或数字 ID + * @returns 含旋转编码的完整方块 ID, 删除/空气返回 0 + */ + setVoxel(x: number, y: number, z: number, voxel: string | number): number; + setVoxel(pos: GameVector3, voxel: string | number): number; + + /** + * 放置方块并指定旋转。返回含旋转编码的完整 ID。 + * Places a block with explicit rotation. + * @param voxel - 方块名称或数字 ID + * @param rotation - 旋转值 0‑3 (或字符串 "0"‑"3") + * @returns 含旋转编码的完整 ID + */ + setVoxel( + x: number, + y: number, + z: number, + voxel: string | number, + rotation: number | string, + ): number; + setVoxel( + pos: GameVector3, + voxel: string | number, + rotation: number | string, + ): number; + + /** + * 放置已含旋转编码的完整 ID 方块。 + * Places a block using a rotation‑encoded full ID (from getVoxelId). + * @param voxel - 完整编码 ID (baseId + rotation * 16384) + */ + setVoxelId(x: number, y: number, z: number, voxel: number): number; + setVoxelId(pos: GameVector3, voxel: number): number; + + // ── 区域操作 / Region operations ── + + /** + * 在两个对角顶点定义的区域内填充方块。 + * Fills a cuboid region with a block. + * @param x1, y1, z1 - 顶点 1 + * @param x2, y2, z2 - 顶点 2 + * @param voxel - 方块名称或 ID + */ + fillVoxel( + x1: number, + y1: number, + z1: number, + x2: number, + y2: number, + z2: number, + voxel: string | number, + ): void; + fillVoxel(pos1: GameVector3, pos2: GameVector3, voxel: string | number): void; + + /** + * 统计区域内指定方块的数量。 + * Counts matching blocks within a cuboid region. + */ + countVoxel( + x1: number, + y1: number, + z1: number, + x2: number, + y2: number, + z2: number, + voxel: string | number, + ): number; + countVoxel( + pos1: GameVector3, + pos2: GameVector3, + voxel: string | number, + ): number; + + // ── 刷怪笼 / Spawner ── + + /** + * 设置刷怪笼的生成实体类型。 + * Sets the spawner entity type at the given position. + * @param x, y, z - 刷怪笼坐标 / spawner coordinates + * @param entityType - 实体类型 ID (如 "minecraft:zombie") + */ + setSpawner(x: number, y: number, z: number, entityType: string): void; + setSpawner(pos: GameVector3, entityType: string): void; +} + +// ================================================================ +// §7 Console — 控制台 +// ================================================================ + +/** + * 服务端控制台输出 — 脚本中通过 `console` 访问。 + * Server console output — accessed via `console` in scripts. + * + * @remarks + * 输出格式: [Box3JS] [projectName] + * 会通过 System.out / System.err 输出到服务端控制台。 + */ +interface GameConsole { + /** 普通日志。Info‑level log. */ + log(...args: unknown[]): void; + + /** 调试日志 (前缀 [DEBUG])。Debug‑level log (prefixed with [DEBUG]). */ + debug(...args: unknown[]): void; + + /** 警告日志 (前缀 [WARN])。Warning‑level log (prefixed with [WARN]). */ + warn(...args: unknown[]): void; + + /** + * 错误日志 (输出到 stderr, 前缀 [ERROR])。 + * Error‑level log (written to stderr, prefixed with [ERROR]). + */ + error(...args: unknown[]): void; + + /** + * 清除控制台 (发送 ANSI 清屏序列)。 + * Clears the console output (sends ANSI clear‑screen sequence). + */ + clear(): void; + + /** + * 断言: 条件为 false 时输出错误。 + * Asserts a condition; logs an error message if the condition is false. + * @param condition - 要测试的条件 / the condition to test + * @param args - 失败时输出的额外参数 / additional values to log on failure + */ + assert(condition: boolean, ...args: unknown[]): void; +} + +// ================================================================ +// §8 Enum Constants — 运行时枚举常量 +// ================================================================ + +/** + * 对话框类型 — 用于 player.dialog()。 + * Dialog type constants for player.dialog(). + */ +declare const GameDialogType: { + readonly TEXT: "TEXT"; + readonly INPUT: "INPUT"; + readonly SELECT: "SELECT"; +}; + +/** + * 按钮类型 — 用于输入绑定。 + * Button type constants for input bindings. + */ +declare const GameButtonType: { + readonly WALK: "WALK"; + readonly RUN: "RUN"; + readonly CROUCH: "CROUCH"; + readonly JUMP: "JUMP"; + readonly DOUBLE_JUMP: "DOUBLE_JUMP"; + readonly FLY: "FLY"; + readonly ACTION0: "ACTION0"; + readonly ACTION1: "ACTION1"; +}; + +/** + * 输入方向 — 用于输入绑定。 + * Input direction constants for input bindings. + */ +declare const GameInputDirection: { + readonly NONE: 0; + readonly VERTICAL: 1; + readonly HORIZONTAL: 2; + readonly BOTH: 3; +}; + +/** + * 相机模式 — 用于 player.cameraMode 属性。 + * Camera mode constants for the player.cameraMode property. + */ +declare const GameCameraMode: { + readonly FIXED: "FIXED"; + readonly FOLLOW: "FOLLOW"; + readonly FPS: "FPS"; + readonly RELATIVE: "RELATIVE"; +}; + +/** + * 玩家移动状态 — player.moveState 的可能返回值。 + * Player movement state constants — possible return values of player.moveState. + */ +declare const GamePlayerMoveState: { + readonly FLYING: "FLYING"; + readonly GROUND: "GROUND"; + readonly SWIM: "SWIM"; + readonly FALL: "FALL"; + readonly JUMP: "JUMP"; + readonly DOUBLE_JUMP: "DOUBLE_JUMP"; +}; + +/** + * 玩家行走状态 — player.walkState 的可能返回值。 + * Player walk state constants — possible return values of player.walkState. + */ +declare const GamePlayerWalkState: { + readonly NONE: "NONE"; + readonly CROUCH: "CROUCH"; + readonly WALK: "WALK"; + readonly RUN: "RUN"; +}; + +// ================================================================ +// §9 Global Declarations — 全局声明 +// ================================================================ + +/** 世界控制与事件 API / World control & events */ +declare const world: GameWorld; + +/** 方块读写 API / Block read & write */ +declare const voxels: GameVoxels; + +/** 持久化存储 API / Persistent key‑value storage */ +declare const storage: GameStorage; + +/** 服务端控制台输出 / Server console output */ +declare const console: GameConsole; + +/** + * CommonJS 模块导入。 + * CommonJS module import. + * + * @remarks + * 从当前项目目录加载 .js 文件 (自动追加 .js 后缀)。 + * Loads a .js file from the current project directory (auto‑appends .js extension). + * 模块通过 Rhino 的 ModuleScope 加载,支持相对路径和嵌套导入。 + * Modules are loaded via Rhino's ModuleScope; relative paths and nested requires are supported. + * + * @param id - 模块标识符 (如 "./state" 或 "./state.js") + * @returns 模块的 exports 对象 + */ +declare function require(id: string): any; + +/** + * 阻塞当前执行线程 (毫秒级)。 + * Blocks the current execution thread for the specified duration. + * + * @warning 会导致服务端卡顿, 谨慎使用。 + * Will lag the server — use sparingly. + * @param ms - 阻塞毫秒数 / sleep duration in milliseconds + */ +declare function sleep(ms: number): void; diff --git a/Box3JS-NeoForge-1.21.1/src/main/templates/META-INF/neoforge.mods.toml b/Box3JS-NeoForge-1.21.1/src/main/templates/META-INF/neoforge.mods.toml new file mode 100644 index 00000000..526db5a3 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/templates/META-INF/neoforge.mods.toml @@ -0,0 +1,95 @@ +# This is an example neoforge.mods.toml file. It contains the data relating to the loading mods. +# There are several mandatory fields (#mandatory), and many more that are optional (#optional). +# The overall format is standard TOML format, v0.5.0. +# Note that there are a couple of TOML lists in this file. +# Find more information on toml format here: https://github.com/toml-lang/toml + +# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml +modLoader="javafml" #mandatory + +# A version range to match for said mod loader - for regular FML @Mod it will be the FML version. This is currently 2. +loaderVersion="${loader_version_range}" #mandatory + +# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. +# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. +license="${mod_license}" + +# A URL to refer people to when problems occur with this mod +#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional + +# A list of mods - how many allowed here is determined by the individual mod loader +[[mods]] #mandatory + +# The modid of the mod +modId="${mod_id}" #mandatory + +# The version number of the mod +version="${mod_version}" #mandatory + +# A display name for the mod +displayName="${mod_name}" #mandatory + +# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforged.net/docs/misc/updatechecker/ +#updateJSONURL="https://change.me.example.invalid/updates.json" #optional + +# A URL for the "homepage" for this mod, displayed in the mod UI +#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional + +# A file name (in the root of the mod JAR) containing a logo for display +#logoFile="examplemod.png" #optional + +# A text field displayed in the mod UI +#credits="" #optional + +# The authors of the mod, displayed in the mod UI (optional) +authors="神岛实验室" + +# The description text for the mod (multi line!) (#mandatory) +description=''' +Box3JS 为 Minecraft 服务端引入 JavaScript 脚本运行时,支持约 100 项 Box3 API 与 90 余项 Minecraft 扩展 API。无需 Java 基础即可编写服务端脚本。 +''' + +# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. +#[[mixins]] +#config="${mod_id}.mixins.json" + +# The [[accessTransformers]] block allows you to declare where your AT file is. +# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg +#[[accessTransformers]] +#file="META-INF/accesstransformer.cfg" + +# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json + +# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. +[[dependencies.${mod_id}]] #optional + # the modid of the dependency + modId="neoforge" #mandatory + # The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). + # 'required' requires the mod to exist, 'optional' does not + # 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning + type="required" #mandatory + # Optional field describing why the dependency is required or why it is incompatible + # reason="..." + # The version range of the dependency + versionRange="[${neo_version},)" #mandatory + # An ordering relationship for the dependency. + # BEFORE - This mod is loaded BEFORE the dependency + # AFTER - This mod is loaded AFTER the dependency + ordering="NONE" + # Side this dependency is applied on - BOTH, CLIENT, or SERVER + side="BOTH" + +# Here's another dependency +[[dependencies.${mod_id}]] + modId="minecraft" + type="required" + # This version range declares a minimum of the current minecraft version up to but not including the next major version + versionRange="${minecraft_version_range}" + ordering="NONE" + side="BOTH" + +# Features are specific properties of the game environment, that you may want to declare you require. This example declares +# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't +# stop your mod loading on the server for example. +#[features.${mod_id}] +#openGLVersion="[3.2,)" diff --git a/README.md b/README.md index d19554f0..36cf3a02 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,61 @@ - `纸`右键模型:复制当前模型参数 - `书`右键模型:粘贴参数到目标模型模型 +### 🧪 Box3JS 脚本引擎 (Beta) + +Box3JS 是一个内置于模组的 JavaScript 脚本引擎(Rhino 引擎),允许服主编写服务端脚本来创建自定义玩法、小游戏和世界交互。所有脚本位于 `config/box3/script/<项目名>/`。 + +**快速开始:** + +```bash +/box3script create mygame # 创建 TypeScript 脚手架 +cd config/box3/script/mygame +npm install && npm run build # 安装依赖并编译 +/box3script sandbox mygame # (推荐) 开启沙盒模式 +/box3script on mygame # 启用并运行脚本 +``` + +**命令一览:** + +| 命令 | 说明 | +|---|---| +| `/box3script` | 列出所有项目及启用/沙盒状态 | +| `/box3script create ` | 创建新脚本项目 (TypeScript 脚手架) | +| `/box3script on ` | 启用并加载指定项目 | +| `/box3script on all` | 一键启用所有项目 | +| `/box3script off ` | 禁用指定项目 | +| `/box3script off all` | 一键禁用所有项目 | +| `/box3script stop` | 停止所有脚本(沙盒项目保留追踪状态) | +| `/box3script stop ` | 停止指定项目(沙盒项目保留追踪状态) | +| `/box3script reload` | 重载所有已启用项目 | +| `/box3script reload ` | 重载指定项目(开发调试用) | +| `/box3script watch` | 切换文件监控(`.js` 变化自动热重载) | +| `/box3script sandbox ` | 切换沙盒模式(开启追踪 / 关闭回滚) | + +> 所有 `` 参数均支持 **Tab 自动补全**。 + +**沙盒系统:** + +沙盒模式开启后自动追踪脚本对世界的所有修改,**持久化**保存(跨 stop/reload 保持),仅手动 `/box3script sandbox ` 关闭时才回滚。追踪内容包括: + +- **方块修改** — `setVoxel`/`setVoxelId`/`fillVoxel`(上限 500 万块,90% 时日志警告) +- **实体状态** — 血量、AI、隐身、发光、无敌、着火、药水效果、标签等 +- **玩家状态** — 游戏模式、飞行能力、移动速度、跳跃力、经验、饱食度、物品栏、护甲、药水效果、位置、维度、重生点 +- **世界状态** — 天气、时间、难度、游戏规则、世界边界 + +关闭沙盒时自动回滚全部修改,并在聊天栏输出恢复摘要:`"restored: 23417 blocks, 83 entities, 2 players, world state"`。 + +**已实现 API:** + +- `world` — 世界控制、事件回调 (16 种)、记分板、Bossbar、队伍、边界、粒子、烟花、射线检测 +- `entity` — 实体属性、AI 寻路、装备、药水效果、标签 +- `player` — 玩家专属:背包、飞行、游戏模式、传送、消息、经验 +- `voxels` — 方块读写、区域填充、刷怪笼 +- `storage` — JSON 数据持久化 +- `console` / `require()` / `sleep()` / `GameVector3` / `GameBounds3` / `GameRGBColor` 等 + +完整 API 文档见 `docs/api/`。 + ### 🔒 命令权限管理 - `/box3import`、`/box3barrier`、`/box3export` 会根据配置要求不同的权限等级才能执行,默认为 `0` 权限等级。