From e88e742e1d7c9d060b2aaeb9e2158b4515bef71e Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Wed, 29 Apr 2026 20:12:54 +0800 Subject: [PATCH 01/17] =?UTF-8?q?feat(box3js):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E8=84=9A=E6=9C=AC=E5=BC=95=E6=93=8E=E6=A8=A1?= =?UTF-8?q?=E7=BB=84=EF=BC=88NeoForge=201.21.1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 从 Box3Blocks 中拆分出 JS 运行时为独立模组: 服务端功能: - Rhino JS 引擎,/box3script 统一命令(eval / file / run / list / on / off / reload / stop) - config/box3/script/<项目>/app.js 自动发现,支持手动开关,默认禁用 - config/box3/storage/ 持久化数据存储 - ~100 项 Box3 风格 API + ~72 项 MC 原生扩展(记分板、队伍、边界、粒子等) --- .claude/settings.local.json | 5 + Box3JS-NeoForge-1.21.1/.gitattributes | 5 + Box3JS-NeoForge-1.21.1/.gitignore | 40 + Box3JS-NeoForge-1.21.1/build.gradle | 207 +++++ .../docs/BOX3_API_MAPPING.md | 409 ++++++++++ Box3JS-NeoForge-1.21.1/gradle.properties | 39 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + Box3JS-NeoForge-1.21.1/gradlew | 251 ++++++ Box3JS-NeoForge-1.21.1/gradlew.bat | 94 +++ Box3JS-NeoForge-1.21.1/settings.gradle | 9 + .../main/java/com/box3lab/box3js/Box3JS.java | 109 +++ .../main/java/com/box3lab/box3js/Config.java | 42 + .../box3lab/box3js/script/Box3JSEntity.java | 334 ++++++++ .../box3lab/box3js/script/Box3JSPlayer.java | 369 +++++++++ .../box3lab/box3js/script/Box3JSStorage.java | 320 ++++++++ .../box3lab/box3js/script/Box3JSVoxels.java | 313 ++++++++ .../box3lab/box3js/script/Box3JSWorld.java | 739 ++++++++++++++++++ .../box3js/script/Box3ScriptCommand.java | 169 ++++ .../box3js/script/Box3ScriptConfig.java | 84 ++ .../box3js/script/Box3ScriptEngine.java | 502 ++++++++++++ .../box3lab/box3js/script/GameBounds3.java | 28 + .../box3lab/box3js/script/GameQuaternion.java | 192 +++++ .../box3lab/box3js/script/GameRGBAColor.java | 88 +++ .../box3lab/box3js/script/GameRGBColor.java | 23 + .../box3lab/box3js/script/GameVector3.java | 69 ++ .../resources/assets/box3js/lang/en_us.json | 13 + .../templates/META-INF/neoforge.mods.toml | 95 +++ 28 files changed, 4555 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 Box3JS-NeoForge-1.21.1/.gitattributes create mode 100644 Box3JS-NeoForge-1.21.1/.gitignore create mode 100644 Box3JS-NeoForge-1.21.1/build.gradle create mode 100644 Box3JS-NeoForge-1.21.1/docs/BOX3_API_MAPPING.md create mode 100644 Box3JS-NeoForge-1.21.1/gradle.properties create mode 100644 Box3JS-NeoForge-1.21.1/gradle/wrapper/gradle-wrapper.jar create mode 100644 Box3JS-NeoForge-1.21.1/gradle/wrapper/gradle-wrapper.properties create mode 100755 Box3JS-NeoForge-1.21.1/gradlew create mode 100644 Box3JS-NeoForge-1.21.1/gradlew.bat create mode 100644 Box3JS-NeoForge-1.21.1/settings.gradle create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Config.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEntity.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSStorage.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSVoxels.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSWorld.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptCommand.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptConfig.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameBounds3.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameQuaternion.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameRGBAColor.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameRGBColor.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameVector3.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/en_us.json create mode 100644 Box3JS-NeoForge-1.21.1/src/main/templates/META-INF/neoforge.mods.toml 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/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/BOX3_API_MAPPING.md b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_MAPPING.md new file mode 100644 index 00000000..55f7a5ef --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_MAPPING.md @@ -0,0 +1,409 @@ +# Box3 API → MC 映射文档 + +## 概览 + +本文档记录了 Box3 平台 JS API 与 Minecraft (NeoForge 1.21.1) 之间的映射关系和实现状态。 + +- **✅ Box3 API** — Box3 原有 API,已接入 MC +- **⬆ MC 扩展** — 非 Box3 原有,利用 MC 特性新增 +- **🚫 不适用** — 无 MC 对应概念 + +### 运行时 + +| 项目 | 说明 | +|---|---| +| 引擎 | Mozilla Rhino 1.7.14 | +| 作用域 | 服务端脚本(S- 脚本) | +| Tick | `ServerTickEvent.Post` | + +--- + +## world (GameWorld) + +### 世界属性 + +| API | 类型 | MC 映射 | 说明 | +|---|---|---|---| +| `world.projectName()` | ✅ | `MinecraftServer.getMotd()` | | +| `world.currentTick()` | ✅ | `MinecraftServer.getTickCount()` | | +| `world.rainDensity` | ✅ | `getRainLevel()` / `setRaining()` | get/set,0.0–1.0 | +| `world.thunderDensity` | ⬆ | `getThunderLevel()` / `setThundering()` | get/set,0.0–1.0 | +| `world.clearWeather()` | ⬆ | `setRaining(false)` + `setThundering(false)` | 清除所有天气 | + +### 时间 + +| API | 类型 | MC 映射 | 说明 | +|---|---|---|---| +| `world.getTime()` | ✅ | `ServerLevel.getDayTime()` | | +| `world.setTime(tick)` | ✅ | `ServerLevel.setDayTime(tick)` | | +| `world.timeScale` | ✅ | `GameRules.RULE_DAYLIGHT` | get/set,0=停止 1=正常 | + +### 难度 / 出生点 + +| API | 类型 | MC 映射 | 说明 | +|---|---|---|---| +| `world.difficulty` | ✅ | `getDifficulty()` / `setDifficulty()` | get 返回名称字符串;set 接受字符串或 0-3 | +| `world.spawnPoint` | ⬆ | `ServerLevel.getSharedSpawnPos()` | 只读 GameVector3 | +| `world.setWorldSpawn(pos)` | ⬆ | `ServerLevel.setDefaultSpawnPos()` | | + +### 实体生成 + +| API | 类型 | MC 映射 | 说明 | +|---|---|---|---| +| `world.spawnEntity(type, pos)` | ✅ | `EntityType.create()` + `addFreshEntity()` | type 为命名空间 ID 字符串,返回 Box3JSEntity | + +### 事件 + +| API | 类型 | 回调参数 | +|---|---|---| +| `world.onTick(handler)` | ✅ | `()` | +| `world.onPlayerJoin(handler)` | ✅ | `(entity)` | +| `world.onPlayerLeave(handler)` | ✅ | `(entity)` | +| `world.onVoxelDestroy(handler)` | ✅ | `(entity, x, y, z, voxel, tick)` | +| `world.onVoxelContact(handler)` | ✅ | `(entity, voxel, x, y, z, axis, force, tick)` | +| `world.onInteract(handler)` | ✅ | `(entity, target, tick)` | +| `world.onChat(handler)` | ✅ | `(entity, message, tick)` | +| `world.onFluidEnter(handler)` | ✅ | `(entity, fluid, x, y, z, tick)` | +| `world.onFluidLeave(handler)` | ✅ | `(entity, fluid, x, y, z, tick)` | +| `world.onEntityContact(handler)` | ✅ | `(entity, other, tick)` | +| `world.onEntitySeparate(handler)` | ✅ | `(entity, other, tick)` | +| `world.onBlockPlace(handler)` | ⬆ | `(entity, x, y, z, voxel, voxelId, tick)` | +| `world.onEntityDeath(handler)` | ⬆ | `(entity, killer, tick)` | +| `world.onPlayerRespawn(handler)` | ⬆ | `(entity)` | +| `world.onBlockActivate(handler)` | ⬆ | `(entity, x, y, z, voxel, tick)` | +| `world.onEntityDamage(handler)` | ⬆ | `(entity, amount, source, attacker, tick)` | + +### 查询 / 聊天 + +| API | 类型 | 说明 | +|---|---|---| +| `world.querySelector(selector)` | ✅ | `"*"` `"#uuid"` `".tag"` | +| `world.querySelectorAll(selector)` | ✅ | 同上,返回数组 | +| `world.say(message)` | ✅ | 全服广播 | + +### 计时器(⬆ MC 扩展) + +| API | 类型 | 说明 | +|---|---|---| +| `world.setTimeout(handler, ticks)` | ⬆ | 延迟执行,返回 timer ID | +| `world.setInterval(handler, ticks)` | ⬆ | 重复执行,返回 timer ID | +| `world.clearTimeout(id)` | ⬆ | 取消 timeout | +| `world.clearInterval(id)` | ⬆ | 取消 interval | + +### 命令 / 查询(⬆ MC 扩展) + +| API | 类型 | 说明 | +|---|---|---| +| `world.runCommand(cmd)` | ⬆ | 以服务器身份执行命令 | +| `world.getEntitiesInArea(pos1, pos2)` | ⬆ | 返回 AABB 区域内所有实体 | +| `world.raycast(origin, dir)` | ⬆ | 射线检测,默认 5 格 | +| `world.raycast(origin, dir, dist)` | ⬆ | 射线检测,自定义距离;返回 `{hit, x, y, z, voxel, entity, normalX/Y/Z, distance}` | +| `world.explode(x, y, z, power)` | ⬆ | 创建爆炸 | +| `world.explode(x, y, z, power, fire)` | ⬆ | 创建爆炸(可引火) | +| `world.playSoundAll(path, x, y, z, vol, pitch)` | ⬆ | 在坐标播放音效给所有玩家 | +| `world.getBiome(x, y, z)` | ⬆ | 获取生物群系命名空间 ID | +--- + +## entity (GameEntity) + +| API | 类型 | 说明 | +|---|---|---| +| `.id` | ✅ | UUID 字符串,只读 | +| `.isPlayer()` | ✅ | | +| `.entityType` | ✅ | 命名空间 ID,只读 | +| `.position` | ✅ | LiveVec3;`.set(x,y,z)` 传送 | +| `.velocity` | ✅ | LiveVec3;`.set(x,y,z)` 修改 | +| `.bounds` | ✅ | 包围盒半尺寸,只读 | +| `.meshInvisible` | ✅ | 同步 `Entity.setInvisible()` | +| `.addTag(tag)` | ✅ | | +| `.hasTag(tag)` | ✅ | | +| `.removeTag(tag)` | ✅ | | +| `.sound(path)` | ✅ | 固定 NOTE_BLOCK_PLING | +| `.hp` | ✅ | LivingEntity 同步 | +| `.maxHp` | ✅ | LivingEntity 同步 | +| `.destroyed` | ✅ | `Entity.isRemoved()`,只读 | +| `.hurt(amount)` | ✅ | | +| `.heal(amount)` | ✅ | | +| `.destroy()` | ✅ | 触发 onDestroy → discard | +| `.remove()` | ⬆ | `discard()` 不触发 onDestroy | +| `.onDestroy(handler)` | ✅ | | +| `.setFire(ticks)` | ⬆ | 点燃实体 | +| `.clearFire()` | ⬆ | 扑灭火焰 | +| `.lookAt(x, y, z)` | ⬆ | 实体面朝指定坐标 | +| `.navigateTo(x, y, z, speed)` | ⬆ | 寻路步行到目标(PathfinderMob) | +| `.setAI(enabled)` | ⬆ | 开关实体 AI | +| `.setEquipment(slot, itemId)` | ⬆ | 给生物穿装备;slot: mainhand/offhand/head/chest/legs/feet | +| `.addEffect(effectId, dur, amp)` | ⬆ | 给任意 LivingEntity 添加药水效果 | +| `.setTarget(entity)` | ⬆ | 设置怪物攻击目标(Mob.setTarget) | +| `.getTarget()` | ⬆ | 获取怪物当前攻击目标 | +| `.clearTarget()` | ⬆ | 清除攻击目标 | +| `.isGlowing()` / `.setGlowing(v)` | ⬆ | 发光效果 | +| `.getNameTag()` / `.setNameTag(n)` | ⬆ | 自定义名称 | +| `.getOnGround()` | ⬆ | 是否在地面 | +| `.getEyePosition()` | ⬆ | 视线高度 GameVector3 | +| `.isInvulnerable()` / `.setInvulnerable(v)` | ⬆ | 无敌状态 | +| `entity.任意字段` | ✅ | 自定义属性,实体生命周期内有效 | + +--- + +## player (GamePlayerEntity) + +### 基本信息 / 外观 + +| API | 类型 | 说明 | +|---|---|---| +| `.name` | ✅ | 只读 | +| `.userId` | ✅ | UUID,只读 | +| `.invisible` | ✅ | get/set | +| `.scale` | ✅ | 只读 | + +### 移动 + +| API | 类型 | 说明 | +|---|---|---| +| `.walkSpeed` | ✅ | `MOVEMENT_SPEED` attribute | +| `.runSpeed` | ✅ | walkSpeed × 1.3 | +| `.jumpPower` | ✅ | `JUMP_STRENGTH` attribute | +| `.moveState` | ✅ | FLYING/SWIM/JUMP/FALL/GROUND | +| `.walkState` | ✅ | CROUCH/RUN/WALK/NONE | + +### 飞行 / 游戏模式 + +| API | 类型 | 说明 | +|---|---|---| +| `.canFly` | ✅ | `PlayerAbilities.mayfly` | +| `.spectator` | ✅ | 只读 | +| `.flySpeed` | ✅ | `PlayerAbilities.flyingSpeed` | +| `.disableFly` | ✅ | set true 时立即禁用飞行 | +| `.gameMode` | ✅ | get 返回名称;set 接受字符串或 0-3 | + +### 相机 + +| API | 类型 | 说明 | +|---|---|---| +| `.cameraMode` | ✅ | FPS 调用 `setCamera(null)` | +| `.cameraEntity` | ✅ | FOLLOW 调用 `setCamera(entity)` | +| `.cameraPitch` | ✅ | `getXRot()` / `setXRot()` | +| `.cameraYaw` | ✅ | `getYRot()` / `setYRot()` | +| `.facingDirection` | ✅ | 只读,`getLookAngle()` | +| `.cameraTarget` | ✅ | 只读,eye + look × 5.0 | + +### 重生 + +| API | 类型 | 说明 | +|---|---|---| +| `.setRespawnPoint(pos)` | ✅ | `player.setRespawnPosition()` | +| `.respawn()` | ✅ | `player.respawn()`(仅死亡时有效) | + +### 踢出 / 传送 + +| API | 类型 | 说明 | +|---|---|---| +| `.kick()` | ✅ | 默认 "Kicked" | +| `.kick(reason)` | ✅ | | +| `.teleport(pos)` | ✅ | | + +### 消息 + +| API | 类型 | 说明 | +|---|---|---| +| `.directMessage(msg)` | ✅ | | +| `.actionBar(msg)` | ✅ | 快捷栏上方 | +| `.title(title, subtitle)` | ⬆ | 默认 fadeIn=10 stay=70 fadeOut=20 | +| `.title(t, s, fIn, stay, fOut)` | ⬆ | 完全参数 | +| `.dialog(config)` | ✅ | 简化版,返回 `{index, value}` | +| `.link(href)` | ✅ | 可点击链接 | +| `.onChat(handler)` | ✅ | 玩家级聊天回调 | + +### 物品 / 效果 / 属性 + +| API | 类型 | 说明 | +|---|---|---| +| `.giveItem(itemId, count)` | ⬆ | 命名空间 ID | +| `.addEffect(effectId, dur, amp)` | ⬆ | 命名空间 ID;duration 为 tick | +| `.clearEffects()` | ⬆ | `removeAllEffects()` | +| `.xp` | ⬆ | 经验等级 get/set | +| `.food` | ⬆ | 饱食度 get/set | +| `.saturation` | ⬆ | 饱和度 get/set | + +### 音效 + +| API | 类型 | 说明 | +|---|---|---| +| `.sound(path)` | ✅ | 固定 NOTE_BLOCK_PLING | +| `.playSound(path, vol, pitch)` | ⬆ | 播放任意 MC 音效 | + +### 维度 / 物品(⬆ MC 扩展) + +| API | 类型 | 说明 | +|---|---|---| +| `.dimension` | ⬆ | 维度 ID,get/set(set 可跨维度传送) | +| `.getHeldItem()` | ⬆ | 主手物品 `{id, count}` | +| `.clearInventory()` | ⬆ | 清空背包 | + +### 命令(⬆ MC 扩展) + +| API | 类型 | 说明 | +|---|---|---| +| `.runCommand(cmd)` | ⬆ | 以玩家身份执行命令 | + +--- + +## 数学类型(全部 ✅) + +- **GameVector3** — `new(x,y,z)`、`.set` `.add` `.sub` `.scale` `.dot` `.mag` `.sqrMag` `.normalize` `.distance` `.lerp` `.equals`、`fromPolar()` +- **GameBounds3** — `new(lo,hi)`、`.intersects` `.contains` +- **GameRGBColor** — `new(r,g,b)` (0.0–1.0)、`.lerp`、`.random()` +- **GameRGBAColor** — `new(r,g,b,a)`、`.set` `.copy` `.clone` `.add/sub/mul/div` `.addEq/subEq/mulEq/divEq` `.lerp` `.equals` `.blendEq` +- **GameQuaternion** — `new(w,x,y,z)`、`.set` `.copy` `.clone` `.add/sub/mul/div` `.inv` `.dot` `.mag` `.sqrMag` `.normalize` `.slerp` `.angle` `.getAxisAngle` `.equals` `.rotateX/Y/Z`、`.fromAxisAngle` `.fromEuler` `.rotationBetween` + +--- + +## 枚举常量(全部 ✅) + +`GameDialogType` `GameButtonType` `GameInputDirection` `GameCameraMode` `GamePlayerMoveState` `GamePlayerWalkState` + +--- + +## voxels (GameVoxels) + +| API | 说明 | +|---|---| +| `voxels.shape` | 只读 | +| `voxels.VoxelTypes` | 方块名称数组 | +| `voxels.id(name)` / `voxels.name(id)` | 名称 ↔ ID | +| `voxels.setVoxel(x,y,z, voxel, rotation?)` | 放置方块;rotation 0-3 | +| `voxels.setVoxelId(x,y,z, voxel)` | voxel 含 rotation 编码 | +| `voxels.getVoxel(x,y,z)` | 基础 ID | +| `voxels.getVoxelId(x,y,z)` | 完整 ID | +| `voxels.getVoxelRotation(x,y,z)` | 0-3 | +| `voxels.fillVoxel(x1,y1,z1, x2,y2,z2, voxel)` | ⬆ 填充矩形区域 | +| `voxels.countVoxel(x1,y1,z1, x2,y2,z2, voxel)` | ⬆ 统计区域内匹配方块数量 | + +--- + +## storage (GameDataStorage) + +| API | 说明 | +|---|---| +| `storage.key` | 空字符串 | +| `storage.getDataStorage(name)` / `getGroupStorage(name)` | 返回 GameDataStorage | +| `store.set(key, value)` / `store.get(key)` | 读写 JSON | +| `store.update(key, handler)` | 回调更新 | +| `store.remove(key)` / `store.increment(key, delta?)` | 删除/递加 | +| `store.list(options)` | 分页排序过滤 | +| `store.destroy()` | 删除存储 | + +--- + +## 命令 + +| 命令 | 说明 | +|---|---| +| `/box3script eval ` | 执行 JS | +| `/box3script file ` | 加载执行 JS 文件 | +| `/box3script run ` | 运行一次项目的 app.js | +| `/box3script list` | 列出所有项目及开关状态 | +| `/box3script on ` | 启用项目 | +| `/box3script off ` | 禁用项目 | +| `/box3script reload` | 重载所有启用项目 | +| `/box3script stop` | 停止所有脚本,清空回调 + +--- + +## 永不能实现的 API + +| API | 原因 | +|---|---| +| `world.animate/getAnimations` | 世界关键帧动画 | +| `world.createEntity/createPlayerEntity` | 动态实体创建(用 `spawnEntity` 替代) | +| `.animation` `.setMotionControl` | Voxa 动作系统 | +| `.enable3DCursor` | 3D 光标 | +| `.boxId` `.userKey` `.querySocial` | Box3 平台账户 | +| `rtc` `analytics` `remoteChannel` `http` `defineParser` | 跨端通讯/语音/分析 | +| 全部客户端 API | 服务端无客户端上下文 | + +--- + +## 命名空间 API (v2) + +`world.*` 下部分功能按分组组织为命名空间调用方式。 + +### world.scoreboard + +| API | 说明 | +|---|---| +| `world.scoreboard.add(name)` | 创建 dummy 记分项 | +| `world.scoreboard.add(name, criteria)` | 创建指定标准记分项 | +| `world.scoreboard.setScore(entityOrName, obj, value)` | 设置分数 | +| `world.scoreboard.getScore(entityOrName, obj)` | 获取分数 | +| `world.scoreboard.show(slot, obj)` | 显示记分板 | +| `world.scoreboard.hide(slot)` | 清除显示槽位 | +| `world.scoreboard.remove(name)` | 删除记分项 | +| `world.scoreboard.list(name)` | 获取所有分数条目 | + +### world.bossbar + +| API | 说明 | +|---|---| +| `world.bossbar.show(name, text, progress, color)` | 显示/更新 Boss 血条 | +| `world.bossbar.remove(name)` | 移除 Boss 血条 | + +### world.team + +| API | 说明 | +|---|---| +| `world.team.create(name, color)` | 创建队伍 | +| `world.team.join(entity, teamName)` | 加入队伍 | +| `world.team.leave(entity)` | 移出队伍 | +| `world.team.remove(name)` | 删除队伍 | +| `world.team.of(entity)` | 获取队伍名称 | + +### world.border + +| API | 说明 | +|---|---| +| `world.border.size()` | 获取边界大小 | +| `world.border.center(x, z)` | 设置边界中心 | +| `world.border.set(size)` | 立即设置边界大小 | +| `world.border.shrink(target, sec)` | 平滑缩圈 | +| `world.border.damage(d)` | 边界外伤害 | +| `world.border.warning(blocks)` | 警告距离 | + +### world.lightning + +| API | 说明 | +|---|---| +| `world.lightning.strike(x, y, z)` | 召唤闪电 | +| `world.lightning.strike(x, y, z, damage)` | 召唤闪电(自定义伤害) | + +### world.firework + +| API | 说明 | +|---|---| +| `world.firework.launch(x, y, z, color, shape)` | 发射烟花 | + +### world.particle + +| API | 说明 | +|---|---| +| `world.particle.spawn(type, x, y, z, ct, dx, dy, dz, spd)` | 生成粒子 | +| `world.particle.circle(x, y, z, radius, type, count)` | 圆形粒子圈 | + +### world.drop + +| API | 说明 | +|---|---| +| `world.drop.item(x, y, z, itemId, count)` | 掉落物品 | + +--- + +## 统计 + +| 状态 | 数量 | +|---|---| +| ✅ Box3 API | ~100 | +| ⬆ MC 扩展 | ~72 | +| 🚫 不适用 | ~80 | + +> 最后更新:2026-04-29 diff --git a/Box3JS-NeoForge-1.21.1/gradle.properties b/Box3JS-NeoForge-1.21.1/gradle.properties new file mode 100644 index 00000000..91fa32fb --- /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 +# 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 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 zcma&OWmKeVvL#I6?i3D%6z=Zs?ofE*?rw#G$eqJB ziT4y8-Y@s9rkH0Tz>ll(^xkcTl)CY?rS&9VNd66Yc)g^6)JcWaY(5$5gt z8gr3SBXUTN;~cBgz&})qX%#!Fxom2Yau_`&8)+6aSN7YY+pS410rRUU*>J}qL0TnJ zRxt*7QeUqTh8j)Q&iavh<}L+$Jqz))<`IfKussVk%%Ah-Ti?Eo0hQH!rK%K=#EAw0 zwq@@~XNUXRnv8$;zv<6rCRJ6fPD^hfrh;0K?n z=p!u^3xOgWZ%f3+?+>H)9+w^$Tn1e;?UpVMJb!!;f)`6f&4|8mr+g)^@x>_rvnL0< zvD0Hu_N>$(Li7|Jgu0mRh&MV+<}`~Wi*+avM01E)Jtg=)-vViQKax!GeDc!xv$^mL z{#OVBA$U{(Zr8~Xm|cP@odkHC*1R8z6hcLY#N@3E-A8XEvpt066+3t9L_6Zg6j@9Q zj$$%~yO-OS6PUVrM2s)(T4#6=JpI_@Uz+!6=GdyVU?`!F=d;8#ZB@(5g7$A0(`eqY z8_i@3w$0*es5mrSjhW*qzrl!_LQWs4?VfLmo1Sd@Ztt53+etwzAT^8ow_*7Jp`Y|l z*UgSEwvxq+FYO!O*aLf-PinZYne7Ib6ny3u>MjQz=((r3NTEeU4=-i0LBq3H-VJH< z^>1RE3_JwrclUn9vb7HcGUaFRA0QHcnE;6)hnkp%lY1UII#WPAv?-;c?YH}LWB8Nl z{sx-@Z;QxWh9fX8SxLZk8;kMFlGD3Jc^QZVL4nO)1I$zQwvwM&_!kW+LMf&lApv#< zur|EyC|U@5OQuph$TC_ZU`{!vJp`13e9alaR0Dbn5ikLFH7>eIz4QbV|C=%7)F=qo z_>M&5N)d)7G(A%c>}UCrW!Ql_6_A{?R7&CL`;!KOb3 z8Z=$YkV-IF;c7zs{3-WDEFJzuakFbd*4LWd<_kBE8~BFcv}js_2OowRNzWCtCQ6&k z{&~Me92$m*@e0ANcWKuz)?YjB*VoSTx??-3Cc0l2U!X^;Bv@m87eKHukAljrD54R+ zE;@_w4NPe1>3`i5Qy*3^E9x#VB6?}v=~qIprrrd5|DFkg;v5ixo0IsBmik8=Y;zv2 z%Bcf%NE$a44bk^`i4VwDLTbX=q@j9;JWT9JncQ!+Y%2&HHk@1~*L8-{ZpY?(-a9J-1~<1ltr9i~D9`P{XTIFWA6IG8c4;6bFw*lzU-{+?b&%OcIoCiw00n>A1ra zFPE$y@>ebbZlf(sN_iWBzQKDV zmmaLX#zK!@ZdvCANfwV}9@2O&w)!5gSgQzHdk2Q`jG6KD7S+1R5&F)j6QTD^=hq&7 zHUW+r^da^%V(h(wonR(j?BOiC!;y=%nJvz?*aW&5E87qq;2z`EI(f zBJNNSMFF9U{sR-af5{IY&AtoGcoG)Iq-S^v{7+t0>7N(KRoPj;+2N5;9o_nxIGjJ@ z7bYQK)bX)vEhy~VL%N6g^NE@D5VtV+Q8U2%{ji_=6+i^G%xeskEhH>Sqr194PJ$fB zu1y^){?9Vkg(FY2h)3ZHrw0Z<@;(gd_dtF#6y_;Iwi{yX$?asr?0N0_B*CifEi7<6 zq`?OdQjCYbhVcg+7MSgIM|pJRu~`g?g3x?Tl+V}#$It`iD1j+!x+!;wS0+2e>#g?Z z*EA^k7W{jO1r^K~cD#5pamp+o@8&yw6;%b|uiT?{Wa=4+9<}aXWUuL#ZwN1a;lQod zW{pxWCYGXdEq9qAmvAB904}?97=re$>!I%wxPV#|f#@A*Y=qa%zHlDv^yWbR03%V0 zprLP+b(#fBqxI%FiF*-n8HtH6$8f(P6!H3V^ysgd8de-N(@|K!A< z^qP}jp(RaM9kQ(^K(U8O84?D)aU(g?1S8iWwe)gqpHCaFlJxb*ilr{KTnu4_@5{K- z)n=CCeCrPHO0WHz)dDtkbZfUfVBd?53}K>C5*-wC4hpDN8cGk3lu-ypq+EYpb_2H; z%vP4@&+c2p;thaTs$dc^1CDGlPG@A;yGR5@$UEqk6p58qpw#7lc<+W(WR;(vr(D>W z#(K$vE#uBkT=*q&uaZwzz=P5mjiee6>!lV?c}QIX%ZdkO1dHg>Fa#xcGT6~}1*2m9 zkc7l3ItD6Ie~o_aFjI$Ri=C!8uF4!Ky7iG9QTrxVbsQroi|r)SAon#*B*{}TB-?=@ z8~jJs;_R2iDd!$+n$%X6FO&PYS{YhDAS+U2o4su9x~1+U3z7YN5o0qUK&|g^klZ6X zj_vrM5SUTnz5`*}Hyts9ADwLu#x_L=nv$Z0`HqN`Zo=V>OQI)fh01n~*a%01%cx%0 z4LTFVjmW+ipVQv5rYcn3;d2o4qunWUY!p+?s~X~(ost@WR@r@EuDOSs8*MT4fiP>! zkfo^!PWJJ1MHgKS2D_hc?Bs?isSDO61>ebl$U*9*QY(b=i&rp3@3GV@z>KzcZOxip z^dzA~44;R~cnhWz7s$$v?_8y-k!DZys}Q?4IkSyR!)C0j$(Gm|t#e3|QAOFaV2}36 z?dPNY;@I=FaCwylc_;~kXlZsk$_eLkNb~TIl8QQ`mmH&$*zwwR8zHU*sId)rxHu*K z;yZWa8UmCwju%aSNLwD5fBl^b0Ux1%q8YR*uG`53Mi<`5uA^Dc6Ync)J3N7;zQ*75)hf%a@{$H+%S?SGT)ks60)?6j$ zspl|4Ad6@%-r1t*$tT(en!gIXTUDcsj?28ZEzz)dH)SV3bZ+pjMaW0oc~rOPZP@g! zb9E+ndeVO_Ib9c_>{)`01^`ZS198 z)(t=+{Azi11$eu%aU7jbwuQrO`vLOixuh~%4z@mKr_Oc;F%Uq01fA)^W&y+g16e?rkLhTxV!EqC%2}sx_1u7IBq|}Be&7WI z4I<;1-9tJsI&pQIhj>FPkQV9{(m!wYYV@i5h?A0#BN2wqlEwNDIq06|^2oYVa7<~h zI_OLan0Do*4R5P=a3H9`s5*>xU}_PSztg`+2mv)|3nIy=5#Z$%+@tZnr> zLcTI!Mxa`PY7%{;KW~!=;*t)R_sl<^b>eNO@w#fEt(tPMg_jpJpW$q_DoUlkY|uo> z0-1{ouA#;t%spf*7VjkK&$QrvwUERKt^Sdo)5@?qAP)>}Y!h4(JQ!7{wIdkA+|)bv z&8hBwoX4v|+fie}iTslaBX^i*TjwO}f{V)8*!dMmRPi%XAWc8<_IqK1jUsApk)+~R zNFTCD-h>M5Y{qTQ&0#j@I@tmXGj%rzhTW5%Bkh&sSc=$Fv;M@1y!zvYG5P2(2|(&W zlcbR1{--rJ&s!rB{G-sX5^PaM@3EqWVz_y9cwLR9xMig&9gq(voeI)W&{d6j1jh&< zARXi&APWE1FQWh7eoZjuP z;vdgX>zep^{{2%hem;e*gDJhK1Hj12nBLIJoL<=0+8SVEBx7!4Ea+hBY;A1gBwvY<)tj~T=H`^?3>zeWWm|LAwo*S4Z%bDVUe z6r)CH1H!(>OH#MXFJ2V(U(qxD{4Px2`8qfFLG+=a;B^~Te_Z!r3RO%Oc#ZAHKQxV5 zRYXxZ9T2A%NVJIu5Pu7!Mj>t%YDO$T@M=RR(~mi%sv(YXVl`yMLD;+WZ{vG9(@P#e zMo}ZiK^7^h6TV%cG+;jhJ0s>h&VERs=tuZz^Tlu~%d{ZHtq6hX$V9h)Bw|jVCMudd zwZ5l7In8NT)qEPGF$VSKg&fb0%R2RnUnqa){)V(X(s0U zkCdVZe6wy{+_WhZh3qLp245Y2RR$@g-!9PjJ&4~0cFSHMUn=>dapv)hy}|y91ZWTV zCh=z*!S3_?`$&-eZ6xIXUq8RGl9oK0BJw*TdU6A`LJqX9eS3X@F)g$jLkBWFscPhR zpCv8#KeAc^y>>Y$k^=r|K(DTC}T$0#jQBOwB#@`P6~*IuW_8JxCG}J4va{ zsZzt}tt+cv7=l&CEuVtjD6G2~_Meh%p4RGuY?hSt?(sreO_F}8r7Kp$qQdvCdZnDQ zxzc*qchE*E2=WK)^oRNa>Ttj`fpvF-JZ5tu5>X1xw)J@1!IqWjq)ESBG?J|ez`-Tc zi5a}GZx|w-h%5lNDE_3ho0hEXMoaofo#Z;$8|2;EDF&*L+e$u}K=u?pb;dv$SXeQM zD-~7P0i_`Wk$#YP$=hw3UVU+=^@Kuy$>6?~gIXx636jh{PHly_a2xNYe1l60`|y!7 z(u%;ILuW0DDJ)2%y`Zc~hOALnj1~txJtcdD#o4BCT68+8gZe`=^te6H_egxY#nZH&P*)hgYaoJ^qtmpeea`35Fw)cy!w@c#v6E29co8&D9CTCl%^GV|X;SpneSXzV~LXyRn-@K0Df z{tK-nDWA!q38M1~`xUIt_(MO^R(yNY#9@es9RQbY@Ia*xHhD&=k^T+ zJi@j2I|WcgW=PuAc>hs`(&CvgjL2a9Rx zCbZyUpi8NWUOi@S%t+Su4|r&UoU|ze9SVe7p@f1GBkrjkkq)T}X%Qo1g!SQ{O{P?m z-OfGyyWta+UCXH+-+(D^%kw#A1-U;?9129at7MeCCzC{DNgO zeSqsV>W^NIfTO~4({c}KUiuoH8A*J!Cb0*sp*w-Bg@YfBIPZFH!M}C=S=S7PLLcIG zs7K77g~W)~^|+mx9onzMm0qh(f~OsDTzVmRtz=aZTllgR zGUn~_5hw_k&rll<4G=G+`^Xlnw;jNYDJz@bE?|r866F2hA9v0-8=JO3g}IHB#b`hy zA42a0>{0L7CcabSD+F7?pGbS1KMvT{@1_@k!_+Ki|5~EMGt7T%u=79F)8xEiL5!EJ zzuxQ`NBliCoJMJdwu|);zRCD<5Sf?Y>U$trQ-;xj6!s5&w=9E7)%pZ+1Nh&8nCCwM zv5>Ket%I?cxr3vVva`YeR?dGxbG@pi{H#8@kFEf0Jq6~K4>kt26*bxv=P&jyE#e$| zDJB_~imk^-z|o!2njF2hL*|7sHCnzluhJjwLQGDmC)Y9 zr9ZN`s)uCd^XDvn)VirMgW~qfn1~SaN^7vcX#K1G`==UGaDVVx$0BQnubhX|{e z^i0}>k-;BP#Szk{cFjO{2x~LjK{^Upqd&<+03_iMLp0$!6_$@TbX>8U-f*-w-ew1?`CtD_0y_Lo|PfKi52p?`5$Jzx0E8`M0 zNIb?#!K$mM4X%`Ry_yhG5k@*+n4||2!~*+&pYLh~{`~o(W|o64^NrjP?-1Lgu?iK^ zTX6u3?#$?R?N!{599vg>G8RGHw)Hx&=|g4599y}mXNpM{EPKKXB&+m?==R3GsIq?G zL5fH={=zawB(sMlDBJ+{dgb)Vx3pu>L=mDV0{r1Qs{0Pn%TpopH{m(By4;{FBvi{I z$}x!Iw~MJOL~&)p93SDIfP3x%ROjg}X{Sme#hiJ&Yk&a;iR}V|n%PriZBY8SX2*;6 z4hdb^&h;Xz%)BDACY5AUsV!($lib4>11UmcgXKWpzRL8r2Srl*9Y(1uBQsY&hO&uv znDNff0tpHlLISam?o(lOp#CmFdH<6HmA0{UwfU#Y{8M+7od8b8|B|7ZYR9f<#+V|ZSaCQvI$~es~g(Pv{2&m_rKSB2QQ zMvT}$?Ll>V+!9Xh5^iy3?UG;dF-zh~RL#++roOCsW^cZ&({6q|?Jt6`?S8=16Y{oH zp50I7r1AC1(#{b`Aq5cw>ypNggHKM9vBx!W$eYIzD!4KbLsZGr2o8>g<@inmS3*>J zx8oG((8f!ei|M@JZB`p7+n<Q}?>h249<`7xJ?u}_n;Gq(&km#1ULN87CeTO~FY zS_Ty}0TgQhV zOh3T7{{x&LSYGQfKR1PDIkP!WnfC1$l+fs@Di+d4O=eVKeF~2fq#1<8hEvpwuqcaH z4A8u~r^gnY3u6}zj*RHjk{AHhrrDqaj?|6GaVJbV%o-nATw}ASFr!f`Oz|u_QPkR# z0mDudY1dZRlk@TyQ?%Eti=$_WNFtLpSx9=S^be{wXINp%MU?a`F66LNU<c;0&ngifmP9i;bj6&hdGMW^Kf8e6ZDXbQD&$QAAMo;OQ)G zW(qlHh;}!ZP)JKEjm$VZjTs@hk&4{?@+NADuYrr!R^cJzU{kGc1yB?;7mIyAWwhbeA_l_lw-iDVi7wcFurf5 z#Uw)A@a9fOf{D}AWE%<`s1L_AwpZ?F!Vac$LYkp<#A!!`XKaDC{A%)~K#5z6>Hv@V zBEqF(D5?@6r3Pwj$^krpPDCjB+UOszqUS;b2n>&iAFcw<*im2(b3|5u6SK!n9Sg4I z0KLcwA6{Mq?p%t>aW0W!PQ>iUeYvNjdKYqII!CE7SsS&Rj)eIw-K4jtI?II+0IdGq z2WT|L3RL?;GtGgt1LWfI4Ka`9dbZXc$TMJ~8#Juv@K^1RJN@yzdLS8$AJ(>g!U9`# zx}qr7JWlU+&m)VG*Se;rGisutS%!6yybi%B`bv|9rjS(xOUIvbNz5qtvC$_JYY+c& za*3*2$RUH8p%pSq>48xR)4qsp!Q7BEiJ*`^>^6INRbC@>+2q9?x(h0bpc>GaNFi$K zPH$6!#(~{8@0QZk=)QnM#I=bDx5vTvjm$f4K}%*s+((H2>tUTf==$wqyoI`oxI7>C z&>5fe)Yg)SmT)eA(|j@JYR1M%KixxC-Eceknf-;N=jJTwKvk#@|J^&5H0c+%KxHUI z6dQbwwVx3p?X<_VRVb2fStH?HH zFR@Mp=qX%#L3XL)+$PXKV|o|#DpHAoqvj6uQKe@M-mnhCSou7Dj4YuO6^*V`m)1lf z;)@e%1!Qg$10w8uEmz{ENb$^%u}B;J7sDd zump}onoD#!l=agcBR)iG!3AF0-63%@`K9G(CzKrm$VJ{v7^O9Ps7Zej|3m= zVXlR&yW6=Y%mD30G@|tf=yC7-#L!16Q=dq&@beWgaIL40k0n% z)QHrp2Jck#evLMM1RGt3WvQ936ZC9vEje0nFMfvmOHVI+&okB_K|l-;|4vW;qk>n~ z+|kk8#`K?x`q>`(f6A${wfw9Cx(^)~tX7<#TpxR#zYG2P+FY~mG{tnEkv~d6oUQA+ z&hNTL=~Y@rF`v-RZlts$nb$3(OL1&@Y11hhL9+zUb6)SP!;CD)^GUtUpCHBE`j1te zAGud@miCVFLk$fjsrcpjsadP__yj9iEZUW{Ll7PPi<$R;m1o!&Xdl~R_v0;oDX2z^!&8}zNGA}iYG|k zmehMd1%?R)u6R#<)B)1oe9TgYH5-CqUT8N7K-A-dm3hbm_W21p%8)H{O)xUlBVb+iUR}-v5dFaCyfSd zC6Bd7=N4A@+Bna=!-l|*_(nWGDpoyU>nH=}IOrLfS+-d40&(Wo*dDB9nQiA2Tse$R z;uq{`X7LLzP)%Y9aHa4YQ%H?htkWd3Owv&UYbr5NUDAH^<l@Z0Cx%`N+B*i!!1u>D8%;Qt1$ zE5O0{-`9gdDxZ!`0m}ywH!;c{oBfL-(BH<&SQ~smbcobU!j49O^f4&IIYh~f+hK*M zZwTp%{ZSAhMFj1qFaOA+3)p^gnXH^=)`NTYgTu!CLpEV2NF=~-`(}7p^Eof=@VUbd z_9U|8qF7Rueg&$qpSSkN%%%DpbV?8E8ivu@ensI0toJ7Eas^jyFReQ1JeY9plb^{m z&eQO)qPLZQ6O;FTr*aJq=$cMN)QlQO@G&%z?BKUs1&I^`lq>=QLODwa`(mFGC`0H< zOlc*|N?B5&!U6BuJvkL?s1&nsi$*5cCv7^j_*l&$-sBmRS85UIrE--7eD8Gr3^+o? zqG-Yl4S&E;>H>k^a0GdUI(|n1`ws@)1%sq2XBdK`mqrNq_b4N{#VpouCXLzNvjoFv zo9wMQ6l0+FT+?%N(ka*;%m~(?338bu32v26!{r)|w8J`EL|t$}TA4q_FJRX5 zCPa{hc_I(7TGE#@rO-(!$1H3N-C0{R$J=yPCXCtGk{4>=*B56JdXU9cQVwB`6~cQZ zf^qK21x_d>X%dT!!)CJQ3mlHA@ z{Prkgfs6=Tz%63$6Zr8CO0Ak3A)Cv#@BVKr&aiKG7RYxY$Yx>Bj#3gJk*~Ps-jc1l z;4nltQwwT4@Z)}Pb!3xM?+EW0qEKA)sqzw~!C6wd^{03-9aGf3Jmt=}w-*!yXupLf z;)>-7uvWN4Unn8b4kfIza-X=x*e4n5pU`HtgpFFd))s$C@#d>aUl3helLom+RYb&g zI7A9GXLRZPl}iQS*d$Azxg-VgcUr*lpLnbPKUV{QI|bsG{8bLG<%CF( zMoS4pRDtLVYOWG^@ox^h8xL~afW_9DcE#^1eEC1SVSb1BfDi^@g?#f6e%v~Aw>@w- zIY0k+2lGWNV|aA*e#`U3=+oBDmGeInfcL)>*!w|*;mWiKNG6wP6AW4-4imN!W)!hE zA02~S1*@Q`fD*+qX@f3!2yJX&6FsEfPditB%TWo3=HA;T3o2IrjS@9SSxv%{{7&4_ zdS#r4OU41~GYMiib#z#O;zohNbhJknrPPZS6sN$%HB=jUnlCO_w5Gw5EeE@KV>soy z2EZ?Y|4RQDDjt5y!WBlZ(8M)|HP<0YyG|D%RqD+K#e7-##o3IZxS^wQ5{Kbzb6h(i z#(wZ|^ei>8`%ta*!2tJzwMv+IFHLF`zTU8E^Mu!R*45_=ccqI};Zbyxw@U%a#2}%f zF>q?SrUa_a4H9l+uW8JHh2Oob>NyUwG=QH~-^ZebU*R@67DcXdz2{HVB4#@edz?B< z5!rQH3O0>A&ylROO%G^fimV*LX7>!%re{_Sm6N>S{+GW1LCnGImHRoF@csnFzn@P0 zM=jld0z%oz;j=>c7mMwzq$B^2mae7NiG}%>(wtmsDXkWk{?BeMpTrIt3Mizq?vRsf zi_WjNp+61uV(%gEU-Vf0;>~vcDhe(dzWdaf#4mH3o^v{0EWhj?E?$5v02sV@xL0l4 zX0_IMFtQ44PfWBbPYN#}qxa%=J%dlR{O!KyZvk^g5s?sTNycWYPJ^FK(nl3k?z-5t z39#hKrdO7V(@!TU)LAPY&ngnZ1MzLEeEiZznn7e-jLCy8LO zu^7_#z*%I-BjS#Pg-;zKWWqX-+Ly$T!4`vTe5ZOV0j?TJVA*2?*=82^GVlZIuH%9s zXiV&(T(QGHHah=s&7e|6y?g+XxZGmK55`wGV>@1U)Th&=JTgJq>4mI&Av2C z)w+kRoj_dA!;SfTfkgMPO>7Dw6&1*Hi1q?54Yng`JO&q->^CX21^PrU^JU#CJ_qhV zSG>afB%>2fx<~g8p=P8Yzxqc}s@>>{g7}F!;lCXvF#RV)^fyYb_)iKVCz1xEq=fJ| z0a7DMCK*FuP=NM*5h;*D`R4y$6cpW-E&-i{v`x=Jbk_xSn@2T3q!3HoAOB`@5Vg6) z{PW|@9o!e;v1jZ2{=Uw6S6o{g82x6g=k!)cFSC*oemHaVjg?VpEmtUuD2_J^A~$4* z3O7HsbA6wxw{TP5Kk)(Vm?gKo+_}11vbo{Tp_5x79P~#F)ahQXT)tSH5;;14?s)On zel1J>1x>+7;g1Iz2FRpnYz;sD0wG9Q!vuzE9yKi3@4a9Nh1!GGN?hA)!mZEnnHh&i zf?#ZEN2sFbf~kV;>K3UNj1&vFhc^sxgj8FCL4v>EOYL?2uuT`0eDH}R zmtUJMxVrV5H{L53hu3#qaWLUa#5zY?f5ozIn|PkMWNP%n zWB5!B0LZB0kLw$k39=!akkE9Q>F4j+q434jB4VmslQ;$ zKiO#FZ`p|dKS716jpcvR{QJkSNfDVhr2%~eHrW;fU45>>snr*S8Vik-5eN5k*c2Mp zyxvX&_cFbB6lODXznHHT|rsURe2!swomtrqc~w5 zymTM8!w`1{04CBprR!_F{5LB+2_SOuZN{b*!J~1ZiPpP-M;);!ce!rOPDLtgR@Ie1 zPreuqm4!H)hYePcW1WZ0Fyaqe%l}F~Orr)~+;mkS&pOhP5Ebb`cnUt!X_QhP4_4p( z8YKQCDKGIy>?WIFm3-}Br2-N`T&FOi?t)$hjphB9wOhBXU#Hb+zm&We_-O)s(wc`2 z8?VsvU;J>Ju7n}uUb3s1yPx_F*|FlAi=Ge=-kN?1;`~6szP%$3B0|8Sqp%ebM)F8v zADFrbeT0cgE>M0DMV@_Ze*GHM>q}wWMzt|GYC%}r{OXRG3Ij&<+nx9;4jE${Fj_r* z`{z1AW_6Myd)i6e0E-h&m{{CvzH=Xg!&(bLYgRMO_YVd8JU7W+7MuGWNE=4@OvP9+ zxi^vqS@5%+#gf*Z@RVyU9N1sO-(rY$24LGsg1>w>s6ST^@)|D9>cT50maXLUD{Fzf zt~tp{OSTEKg3ZSQyQQ5r51){%=?xlZ54*t1;Ow)zLe3i?8tD8YyY^k%M)e`V*r+vL zPqUf&m)U+zxps+NprxMHF{QSxv}>lE{JZETNk1&F+R~bp{_T$dbXL2UGnB|hgh*p4h$clt#6;NO~>zuyY@C-MD@)JCc5XrYOt`wW7! z_ti2hhZBMJNbn0O-uTxl_b6Hm313^fG@e;RrhIUK9@# z+DHGv_Ow$%S8D%RB}`doJjJy*aOa5mGHVHz0e0>>O_%+^56?IkA5eN+L1BVCp4~m=1eeL zb;#G!#^5G%6Mw}r1KnaKsLvJB%HZL)!3OxT{k$Yo-XrJ?|7{s4!H+S2o?N|^Z z)+?IE9H7h~Vxn5hTis^3wHYuOU84+bWd)cUKuHapq=&}WV#OxHpLab`NpwHm8LmOo zjri+!k;7j_?FP##CpM+pOVx*0wExEex z@`#)K<-ZrGyArK;a%Km`^+We|eT+#MygHOT6lXBmz`8|lyZOwL1+b+?Z$0OhMEp3R z&J=iRERpv~TC=p2-BYLC*?4 zxvPs9V@g=JT0>zky5Poj=fW_M!c)Xxz1<=&_ZcL=LMZJqlnO1P^xwGGW*Z+yTBvbV z-IFe6;(k1@$1;tS>{%pXZ_7w+i?N4A2=TXnGf=YhePg8bH8M|Lk-->+w8Y+FjZ;L=wSGwxfA`gqSn)f(XNuSm>6Y z@|#e-)I(PQ^G@N`%|_DZSb4_pkaEF0!-nqY+t#pyA>{9^*I-zw4SYA1_z2Bs$XGUZbGA;VeMo%CezHK0lO={L%G)dI-+8w?r9iexdoB{?l zbJ}C?huIhWXBVs7oo{!$lOTlvCLZ_KN1N+XJGuG$rh<^eUQIqcI7^pmqhBSaOKNRq zrx~w^?9C?*&rNwP_SPYmo;J-#!G|{`$JZK7DxsM3N^8iR4vvn>E4MU&Oe1DKJvLc~ zCT>KLZ1;t@My zRj_2hI^61T&LIz)S!+AQIV23n1>ng+LUvzv;xu!4;wpqb#EZz;F)BLUzT;8UA1x*6vJ zicB!3Mj03s*kGV{g`fpC?V^s(=JG-k1EMHbkdP4P*1^8p_TqO|;!Zr%GuP$8KLxuf z=pv*H;kzd;P|2`JmBt~h6|GxdU~@weK5O=X&5~w$HpfO}@l-T7@vTCxVOwCkoPQv8 z@aV_)I5HQtfs7^X=C03zYmH4m0S!V@JINm6#(JmZRHBD?T!m^DdiZJrhKpBcur2u1 zf9e4%k$$vcFopK5!CC`;ww(CKL~}mlxK_Pv!cOsFgVkNIghA2Au@)t6;Y3*2gK=5d z?|@1a)-(sQ%uFOmJ7v2iG&l&m^u&^6DJM#XzCrF%r>{2XKyxLD2rgWBD;i(!e4InDQBDg==^z;AzT2z~OmV0!?Z z0S9pX$+E;w3WN;v&NYT=+G8hf=6w0E1$0AOr61}eOvE8W1jX%>&Mjo7&!ulawgzLH zbcb+IF(s^3aj12WSi#pzIpijJJzkP?JzRawnxmNDSUR#7!29vHULCE<3Aa#be}ie~d|!V+ z%l~s9Odo$G&fH!t!+`rUT0T9DulF!Yq&BfQWFZV1L9D($r4H(}Gnf6k3^wa7g5|Ws zj7%d`!3(0bb55yhC6@Q{?H|2os{_F%o=;-h{@Yyyn*V7?{s%Grvpe!H^kl6tF4Zf5 z{Jv1~yZ*iIWL_9C*8pBMQArfJJ0d9Df6Kl#wa}7Xa#Ef_5B7=X}DzbQXVPfCwTO@9+@;A^Ti6il_C>g?A-GFwA0#U;t4;wOm-4oS})h z5&on>NAu67O?YCQr%7XIzY%LS4bha9*e*4bU4{lGCUmO2UQ2U)QOqClLo61Kx~3dI zmV3*(P6F_Tr-oP%x!0kTnnT?Ep5j;_IQ^pTRp=e8dmJtI4YgWd0}+b2=ATkOhgpXe z;jmw+FBLE}UIs4!&HflFr4)vMFOJ19W4f2^W(=2)F%TAL)+=F>IE$=e=@j-*bFLSg z)wf|uFQu+!=N-UzSef62u0-C8Zc7 zo6@F)c+nZA{H|+~7i$DCU0pL{0Ye|fKLuV^w!0Y^tT$isu%i1Iw&N|tX3kwFKJN(M zXS`k9js66o$r)x?TWL}Kxl`wUDUpwFx(w4Yk%49;$sgVvT~n8AgfG~HUcDt1TRo^s zdla@6heJB@JV z!vK;BUMznhzGK6PVtj0)GB=zTv6)Q9Yt@l#fv7>wKovLobMV-+(8)NJmyF8R zcB|_K7=FJGGn^X@JdFaat0uhKjp3>k#^&xE_}6NYNG?kgTp>2Iu?ElUjt4~E-?`Du z?mDCS9wbuS%fU?5BU@Ijx>1HG*N?gIP+<~xE4u=>H`8o((cS5M6@_OK%jSjFHirQK zN9@~NXFx*jS{<|bgSpC|SAnA@I)+GB=2W|JJChLI_mx+-J(mSJ!b)uUom6nH0#2^(L@JBlV#t zLl?j54s`Y3vE^c_3^Hl0TGu*tw_n?@HyO@ZrENxA+^!)OvUX28gDSF*xFtQzM$A+O zCG=n#6~r|3zt=8%GuG} z<#VCZ%2?3Q(Ad#Y7GMJ~{U3>E{5e@z6+rgZLX{Cxk^p-7dip^d29;2N1_mm4QkASo z-L`GWWPCq$uCo;X_BmGIpJFBlhl<8~EG{vOD1o|X$aB9KPhWO_cKiU*$HWEgtf=fn zsO%9bp~D2c@?*K9jVN@_vhR03>M_8h!_~%aN!Cnr?s-!;U3SVfmhRwk11A^8Ns`@KeE}+ zN$H}a1U6E;*j5&~Og!xHdfK5M<~xka)x-0N)K_&e7AjMz`toDzasH+^1bZlC!n()crk9kg@$(Y{wdKvbuUd04N^8}t1iOgsKF zGa%%XWx@WoVaNC1!|&{5ZbkopFre-Lu(LCE5HWZBoE#W@er9W<>R=^oYxBvypN#x3 zq#LC8&q)GFP=5^-bpHj?LW=)-g+3_)Ylps!3^YQ{9~O9&K)xgy zMkCWaApU-MI~e^cV{Je75Qr7eF%&_H)BvfyKL=gIA>;OSq(y z052BFz3E(Prg~09>|_Z@!qj}@;8yxnw+#Ej0?Rk<y}4ghbD569B{9hSFr*^ygZ zr6j7P#gtZh6tMk6?4V$*Jgz+#&ug;yOr>=qdI#9U&^am2qoh4Jy}H2%a|#Fs{E(5r z%!ijh;VuGA6)W)cJZx+;9Bp1LMUzN~x_8lQ#D3+sL{be-Jyeo@@dv7XguJ&S5vrH` z>QxOMWn7N-T!D@1(@4>ZlL^y5>m#0!HKovs12GRav4z!>p(1~xok8+_{| z#Ae4{9#NLh#Vj2&JuIn5$d6t@__`o}umFo(n0QxUtd2GKCyE+erwXY?`cm*h&^9*8 zJ+8x6fRZI-e$CRygofIQN^dWysCxgkyr{(_oBwwSRxZora1(%(aC!5BTtj^+YuevI zx?)H#(xlALUp6QJ!=l9N__$cxBZ5p&7;qD3PsXRFVd<({Kh+mShFWJNpy`N@ab7?9 zv5=klvCJ4bx|-pvOO2-+G)6O?$&)ncA#Urze2rlBfp#htudhx-NeRnJ@u%^_bfw4o z4|{b8SkPV3b>Wera1W(+N@p9H>dc6{cnkh-sgr?e%(YkWvK+0YXVwk0=d`)}*47*B z5JGkEdVix!w7-<%r0JF~`ZMMPe;f0EQHuYHxya`puazyph*ZSb1mJAt^k4549BfS; zK7~T&lRb=W{s&t`DJ$B}s-eH1&&-wEOH1KWsKn0a(ZI+G!v&W4A*cl>qAvUv6pbUR z#(f#EKV8~hk&8oayBz4vaswc(?qw1vn`yC zZQDl2PCB-&Uu@g9ZQHhO+v(W0bNig{-k0;;`+wM@#@J)8r?qOYs#&vUna8ILxN7S{ zp1s41KnR8miQJtJtOr|+qk}wrLt+N*z#5o`TmD1)E&QD(Vh&pjZJ_J*0!8dy_ z>^=@v=J)C`x&gjqAYu`}t^S=DFCtc0MkBU2zf|69?xW`Ck~(6zLD)gSE{7n~6w8j_ zoH&~$ED2k5-yRa0!r8fMRy z;QjBYUaUnpd}mf%iVFPR%Dg9!d>g`01m~>2s))`W|5!kc+_&Y>wD@@C9%>-lE`WB0 zOIf%FVD^cj#2hCkFgi-fgzIfOi+ya)MZK@IZhHT5FVEaSbv-oDDs0W)pA0&^nM0TW zmgJmd7b1R7b0a`UwWJYZXp4AJPteYLH>@M|xZFKwm!t3D3&q~av?i)WvAKHE{RqpD{{%OhYkK?47}+}` zrR2(Iv9bhVa;cDzJ%6ntcSbx7v7J@Y4x&+eWSKZ*eR7_=CVIUSB$^lfYe@g+p|LD{ zPSpQmxx@b$%d!05|H}WzBT4_cq?@~dvy<7s&QWtieJ9)hd4)$SZz}#H2UTi$CkFWW|I)v_-NjuH!VypONC=1`A=rm_jfzQ8Fu~1r8i{q-+S_j$ z#u^t&Xnfi5tZtl@^!fUJhx@~Cg0*vXMK}D{>|$#T*+mj(J_@c{jXBF|rm4-8%Z2o! z2z0o(4%8KljCm^>6HDK!{jI7p+RAPcty_~GZ~R_+=+UzZ0qzOwD=;YeZt*?3%UGdr z`c|BPE;yUbnyARUl&XWSNJ<+uRt%!xPF&K;(l$^JcA_CMH6)FZt{>6ah$|(9$2fc~ z=CD00uHM{qv;{Zk9FR0~u|3|Eiqv9?z2#^GqylT5>6JNZwKqKBzzQpKU2_pmtD;CT zi%Ktau!Y2Tldfu&b0UgmF(SSBID)15*r08eoUe#bT_K-G4VecJL2Pa=6D1K6({zj6 za(2Z{r!FY5W^y{qZ}08+h9f>EKd&PN90f}Sc0ejf%kB4+f#T8Q1=Pj=~#pi$U zp#5rMR%W25>k?<$;$x72pkLibu1N|jX4cWjD3q^Pk3js!uK6h7!dlvw24crL|MZs_ zb%Y%?Fyp0bY0HkG^XyS76Ts*|Giw{31LR~+WU5NejqfPr73Rp!xQ1mLgq@mdWncLy z%8}|nzS4P&`^;zAR-&nm5f;D-%yNQPwq4N7&yULM8bkttkD)hVU>h>t47`{8?n2&4 zjEfL}UEagLUYwdx0sB2QXGeRmL?sZ%J!XM`$@ODc2!y|2#7hys=b$LrGbvvjx`Iqi z&RDDm3YBrlKhl`O@%%&rhLWZ*ABFz2nHu7k~3@e4)kO3%$=?GEFUcCF=6-1n!x^vmu+Ai*amgXH+Rknl6U>#9w;A} zn2xanZSDu`4%%x}+~FG{Wbi1jo@wqBc5(5Xl~d0KW(^Iu(U3>WB@-(&vn_PJt9{1`e9Iic@+{VPc`vP776L*viP{wYB2Iff8hB%E3|o zGMOu)tJX!`qJ}ZPzq7>=`*9TmETN7xwU;^AmFZ-ckZjV5B2T09pYliaqGFY|X#E-8 z20b>y?(r-Fn5*WZ-GsK}4WM>@TTqsxvSYWL6>18q8Q`~JO1{vLND2wg@58OaU!EvT z1|o+f1mVXz2EKAbL!Q=QWQKDZpV|jznuJ}@-)1&cdo z^&~b4Mx{*1gurlH;Vhk5g_cM&6LOHS2 zRkLfO#HabR1JD4Vc2t828dCUG#DL}f5QDSBg?o)IYYi@_xVwR2w_ntlpAW0NWk$F1 z$If?*lP&Ka1oWfl!)1c3fl`g*lMW3JOn#)R1+tfwrs`aiFUgz3;XIJ>{QFxLCkK30 zNS-)#DON3yb!7LBHQJ$)4y%TN82DC2-9tOIqzhZ27@WY^<6}vXCWcR5iN{LN8{0u9 zNXayqD=G|e?O^*ms*4P?G%o@J1tN9_76e}E#66mr89%W_&w4n66~R;X_vWD(oArwj z4CpY`)_mH2FvDuxgT+akffhX0b_slJJ*?Jn3O3~moqu2Fs1oL*>7m=oVek2bnprnW zixkaIFU%+3XhNA@@9hyhFwqsH2bM|`P?G>i<-gy>NflhrN{$9?LZ1ynSE_Mj0rADF zhOz4FnK}wpLmQuV zgO4_Oz9GBu_NN>cPLA=`SP^$gxAnj;WjJnBi%Q1zg`*^cG;Q)#3Gv@c^j6L{arv>- zAW%8WrSAVY1sj$=umcAf#ZgC8UGZGoamK}hR7j6}i8#np8ruUlvgQ$j+AQglFsQQq zOjyHf22pxh9+h#n$21&$h?2uq0>C9P?P=Juw0|;oE~c$H{#RGfa>| zj)Iv&uOnaf@foiBJ}_;zyPHcZt1U~nOcNB{)og8Btv+;f@PIT*xz$x!G?u0Di$lo7 zOugtQ$Wx|C($fyJTZE1JvR~i7LP{ zbdIwqYghQAJi9p}V&$=*2Azev$6K@pyblphgpv8^9bN!?V}{BkC!o#bl&AP!3DAjM zmWFsvn2fKWCfjcAQmE+=c3Y7j@#7|{;;0f~PIodmq*;W9Fiak|gil6$w3%b_Pr6K_ zJEG@&!J%DgBZJDCMn^7mk`JV0&l07Bt`1ymM|;a)MOWz*bh2#d{i?SDe9IcHs7 zjCrnyQ*Y5GzIt}>`bD91o#~5H?4_nckAgotN{2%!?wsSl|LVmJht$uhGa+HiH>;av z8c?mcMYM7;mvWr6noUR{)gE!=i7cZUY7e;HXa221KkRoc2UB>s$Y(k%NzTSEr>W(u z<(4mcc)4rB_&bPzX*1?*ra%VF}P1nwiP5cykJ&W{!OTlz&Td0pOkVp+wc z@k=-Hg=()hNg=Q!Ub%`BONH{ z_=ZFgetj@)NvppAK2>8r!KAgi>#%*7;O-o9MOOfQjV-n@BX6;Xw;I`%HBkk20v`qoVd0)}L6_49y1IhR z_OS}+eto}OPVRn*?UHC{eGyFU7JkPz!+gX4P>?h3QOwGS63fv4D1*no^6PveUeE5% zlehjv_3_^j^C({a2&RSoVlOn71D8WwMu9@Nb@=E_>1R*ve3`#TF(NA0?d9IR_tm=P zOP-x;gS*vtyE1Cm zG0L?2nRUFj#aLr-R1fX*$sXhad)~xdA*=hF3zPZhha<2O$Ps+F07w*3#MTe?)T8|A!P!v+a|ot{|^$q(TX`35O{WI0RbU zCj?hgOv=Z)xV?F`@HKI11IKtT^ocP78cqHU!YS@cHI@{fPD?YXL)?sD~9thOAv4JM|K8OlQhPXgnevF=F7GKD2#sZW*d za}ma31wLm81IZxX(W#A9mBvLZr|PoLnP>S4BhpK8{YV_}C|p<)4#yO{#ISbco92^3 zv&kCE(q9Wi;9%7>>PQ!zSkM%qqqLZW7O`VXvcj;WcJ`2~v?ZTYB@$Q&^CTfvy?1r^ z;Cdi+PTtmQwHX_7Kz?r#1>D zS5lWU(Mw_$B&`ZPmqxpIvK<~fbXq?x20k1~9az-Q!uR78mCgRj*eQ>zh3c$W}>^+w^dIr-u{@s30J=)1zF8?Wn|H`GS<=>Om|DjzC{}Jt?{!fSJe*@$H zg>wFnlT)k#T?LslW zu$^7Uy~$SQ21cE?3Ijl+bLfuH^U5P^$@~*UY#|_`uvAIe(+wD2eF}z_y!pvomuVO; zS^9fbdv)pcm-B@CW|Upm<7s|0+$@@<&*>$a{aW+oJ%f+VMO<#wa)7n|JL5egEgoBv zl$BY(NQjE0#*nv=!kMnp&{2Le#30b)Ql2e!VkPLK*+{jv77H7)xG7&=aPHL7LK9ER z5lfHxBI5O{-3S?GU4X6$yVk>lFn;ApnwZybdC-GAvaznGW-lScIls-P?Km2mF>%B2 zkcrXTk+__hj-3f48U%|jX9*|Ps41U_cd>2QW81Lz9}%`mTDIhE)jYI$q$ma7Y-`>% z8=u+Oftgcj%~TU}3nP8&h7k+}$D-CCgS~wtWvM|UU77r^pUw3YCV80Ou*+bH0!mf0 zxzUq4ed6y>oYFz7+l18PGGzhB^pqSt)si=9M>~0(Bx9*5r~W7sa#w+_1TSj3Jn9mW zMuG9BxN=}4645Cpa#SVKjFst;9UUY@O<|wpnZk$kE+to^4!?0@?Cwr3(>!NjYbu?x z1!U-?0_O?k!NdM^-rIQ8p)%?M+2xkhltt*|l=%z2WFJhme7*2xD~@zk#`dQR$6Lmd zb3LOD4fdt$Cq>?1<%&Y^wTWX=eHQ49Xl_lFUA(YQYHGHhd}@!VpYHHm=(1-O=yfK#kKe|2Xc*9}?BDFN zD7FJM-AjVi)T~OG)hpSWqH>vlb41V#^G2B_EvYlWhDB{Z;Q9-0)ja(O+By`31=biA zG&Fs#5!%_mHi|E4Nm$;vVQ!*>=_F;ZC=1DTPB#CICS5fL2T3XmzyHu?bI;m7D4@#; ztr~;dGYwb?m^VebuULtS4lkC_7>KCS)F@)0OdxZIFZp@FM_pHnJes8YOvwB|++#G( z&dm*OP^cz95Wi15vh`Q+yB>R{8zqEhz5of>Po$9LNE{xS<)lg2*roP*sQ}3r3t<}; zPbDl{lk{pox~2(XY5=qg0z!W-x^PJ`VVtz$git7?)!h>`91&&hESZy1KCJ2nS^yMH z!=Q$eTyRi68rKxdDsdt+%J_&lapa{ds^HV9Ngp^YDvtq&-Xp}60B_w@Ma>_1TTC;^ zpbe!#gH}#fFLkNo#|`jcn?5LeUYto%==XBk6Ik0kc4$6Z+L3x^4=M6OI1=z5u#M%0 z0E`kevJEpJjvvN>+g`?gtnbo$@p4VumliZV3Z%CfXXB&wPS^5C+7of2tyVkMwNWBiTE2 z8CdPu3i{*vR-I(NY5syRR}I1TJOV@DJy-Xmvxn^IInF>Tx2e)eE9jVSz69$6T`M9-&om!T+I znia!ZWJRB28o_srWlAxtz4VVft8)cYloIoVF=pL zugnk@vFLXQ_^7;%hn9x;Vq?lzg7%CQR^c#S)Oc-8d=q_!2ZVH764V z!wDKSgP}BrVV6SfCLZnYe-7f;igDs9t+K*rbMAKsp9L$Kh<6Z;e7;xxced zn=FGY<}CUz31a2G}$Q(`_r~75PzM4l_({Hg&b@d8&jC}B?2<+ed`f#qMEWi z`gm!STV9E4sLaQX+sp5Nu9*;9g12naf5?=P9p@H@f}dxYprH+3ju)uDFt^V{G0APn zS;16Dk{*fm6&BCg#2vo?7cbkkI4R`S9SSEJ=#KBk3rl69SxnCnS#{*$!^T9UUmO#&XXKjHKBqLdt^3yVvu8yn|{ zZ#%1CP)8t-PAz(+_g?xyq;C2<9<5Yy<~C74Iw(y>uUL$+$mp(DRcCWbCKiGCZw@?_ zdomfp+C5xt;j5L@VfhF*xvZdXwA5pcdsG>G<8II-|1dhAgzS&KArcb0BD4ZZ#WfiEY{hkCq5%z9@f|!EwTm;UEjKJsUo696V>h zy##eXYX}GUu%t{Gql8vVZKkNhQeQ4C%n|RmxL4ee5$cgwlU+?V7a?(jI#&3wid+Kz5+x^G!bb#$q>QpR#BZ}Xo5UW^ zD&I`;?(a}Oys7-`I^|AkN?{XLZNa{@27Dv^s4pGowuyhHuXc zuctKG2x0{WCvg_sGN^n9myJ}&FXyGmUQnW7fR$=bj$AHR88-q$D!*8MNB{YvTTEyS zn22f@WMdvg5~o_2wkjItJN@?mDZ9UUlat2zCh(zVE=dGi$rjXF7&}*sxac^%HFD`Y zTM5D3u5x**{bW!68DL1A!s&$2XG@ytB~dX-?BF9U@XZABO`a|LM1X3HWCllgl0+uL z04S*PX$%|^WAq%jkzp~%9HyYIF{Ym?k)j3nMwPZ=hlCg9!G+t>tf0o|J2%t1 ztC+`((dUplgm3`+0JN~}&FRRJ3?l*>Y&TfjS>!ShS`*MwO{WIbAZR#<%M|4c4^dY8 z{Rh;-!qhY=dz5JthbWoovLY~jNaw>%tS4gHVlt5epV8ekXm#==Po$)}mh^u*cE>q7*kvX&gq)(AHoItMYH6^s6f(deNw%}1=7O~bTHSj1rm2|Cq+3M z93djjdomWCTCYu!3Slx2bZVy#CWDozNedIHbqa|otsUl+ut?>a;}OqPfQA05Yim_2 zs@^BjPoFHOYNc6VbNaR5QZfSMh2S*`BGwcHMM(1@w{-4jVqE8Eu0Bi%d!E*^Rj?cR z7qgxkINXZR)K^=fh{pc0DCKtrydVbVILI>@Y0!Jm>x-xM!gu%dehm?cC6ok_msDVA*J#{75%4IZt}X|tIVPReZS#aCvuHkZxc zHVMtUhT(wp09+w9j9eRqz~LtuSNi2rQx_QgQ(}jBt7NqyT&ma61ldD(s9x%@q~PQl zp6N*?=N$BtvjQ_xIT{+vhb1>{pM0Arde0!X-y))A4znDrVx8yrP3B1(7bKPE5jR@5 zwpzwT4cu~_qUG#zYMZ_!2Tkl9zP>M%cy>9Y(@&VoB84#%>amTAH{(hL4cDYt!^{8L z645F>BWO6QaFJ-{C-i|-d%j7#&7)$X7pv#%9J6da#9FB5KyDhkA+~)G0^87!^}AP>XaCSScr;kL;Z%RSPD2CgoJ;gpYT5&6NUK$86$T?jRH=w8nI9Z534O?5fk{kd z`(-t$8W|#$3>xoMfXvV^-A(Q~$8SKDE^!T;J+rQXP71XZ(kCCbP%bAQ1|%$%Ov9_a zyC`QP3uPvFoBqr_+$HenHklqyIr>PU_Fk5$2C+0eYy^~7U&(!B&&P2%7#mBUhM!z> z_B$Ko?{Pf6?)gpYs~N*y%-3!1>o-4;@1Zz9VQHh)j5U1aL-Hyu@1d?X;jtDBNk*vMXPn@ z+u@wxHN*{uHR!*g*4Xo&w;5A+=Pf9w#PeZ^x@UD?iQ&${K2c}UQgLRik-rKM#Y5rdDphdcNTF~cCX&9ViRP}`>L)QA4zNXeG)KXFzSDa6 zd^St;inY6J_i=5mcGTx4_^Ys`M3l%Q==f>{8S1LEHn{y(kbxn5g1ezt4CELqy)~TV6{;VW>O9?5^ ztcoxHRa0jQY7>wwHWcxA-BCwzsP>63Kt&3fy*n#Cha687CQurXaRQnf5wc9o8v7Rw zNwGr2fac;Wr-Ldehn7tF^(-gPJwPt@VR1f;AmKgxN&YPL;j=0^xKM{!wuU|^mh3NE zy35quf}MeL!PU;|{OW_x$TBothLylT-J>_x6p}B_jW1L>k)ps6n%7Rh z96mPkJIM0QFNYUM2H}YF5bs%@Chs6#pEnloQhEl?J-)es!(SoJpEPoMTdgA14-#mC zghayD-DJWtUu`TD8?4mR)w5E`^EHbsz2EjH5aQLYRcF{l7_Q5?CEEvzDo(zjh|BKg z3aJl_n#j&eFHsUw4~lxqnr!6NL*se)6H=A+T1e3xUJGQrd}oSPwSy5+$tt{2t5J5@(lFxl43amsARG74iyNC}uuS zd2$=(r6RdamdGx^eatX@F2D8?U23tDpR+Os?0Gq2&^dF+$9wiWf?=mDWfjo4LfRwL zI#SRV9iSz>XCSgEj!cW&9H-njJopYiYuq|2w<5R2!nZ27DyvU4UDrHpoNQZiGPkp@ z1$h4H46Zn~eqdj$pWrv;*t!rTYTfZ1_bdkZmVVIRC21YeU$iS-*XMNK`#p8Z_DJx| zk3Jssf^XP7v0X?MWFO{rACltn$^~q(M9rMYoVxG$15N;nP)A98k^m3CJx8>6}NrUd@wp-E#$Q0uUDQT5GoiK_R{ z<{`g;8s>UFLpbga#DAf%qbfi`WN1J@6IA~R!YBT}qp%V-j!ybkR{uY0X|x)gmzE0J z&)=eHPjBxJvrZSOmt|)hC+kIMI;qgOnuL3mbNR0g^<%|>9x7>{}>a2qYSZAGPt4it?8 zNcLc!Gy0>$jaU?}ZWxK78hbhzE+etM`67*-*x4DN>1_&{@5t7_c*n(qz>&K{Y?10s zXsw2&nQev#SUSd|D8w7ZD2>E<%g^; zV{yE_O}gq?Q|zL|jdqB^zcx7vo(^})QW?QKacx$yR zhG|XH|8$vDZNIfuxr-sYFR{^csEI*IM#_gd;9*C+SysUFejP0{{z7@P?1+&_o6=7V|EJLQun^XEMS)w(=@eMi5&bbH*a0f;iC~2J74V2DZIlLUHD&>mlug5+v z6xBN~8-ovZylyH&gG#ptYsNlT?-tzOh%V#Y33zlsJ{AIju`CjIgf$@gr8}JugRq^c zAVQ3;&uGaVlVw}SUSWnTkH_6DISN&k2QLMBe9YU=sA+WiX@z)FoSYX`^k@B!j;ZeC zf&**P?HQG6Rk98hZ*ozn6iS-dG}V>jQhb3?4NJB*2F?6N7Nd;EOOo;xR7acylLaLy z9)^lykX39d@8@I~iEVar4jmjjLWhR0d=EB@%I;FZM$rykBNN~jf>#WbH4U{MqhhF6 zU??@fSO~4EbU4MaeQ_UXQcFyO*Rae|VAPLYMJEU`Q_Q_%s2*>$#S^)&7er+&`9L=1 z4q4ao07Z2Vsa%(nP!kJ590YmvrWg+YrgXYs_lv&B5EcoD`%uL79WyYA$0>>qi6ov7 z%`ia~J^_l{p39EY zv>>b}Qs8vxsu&WcXEt8B#FD%L%ZpcVtY!rqVTHe;$p9rbb5O{^rFMB>auLn-^;s+-&P1#h~mf~YLg$8M9 zZ4#87;e-Y6x6QO<{McUzhy(%*6| z)`D~A(TJ$>+0H+mct(jfgL4x%^oC^T#u(bL)`E2tBI#V1kSikAWmOOYrO~#-cc_8! zCe|@1&mN2{*ceeiBldHCdrURk4>V}79_*TVP3aCyV*5n@jiNbOm+~EQ_}1#->_tI@ zqXv+jj2#8xJtW508rzFrYcJxoek@iW6SR@1%a%Bux&;>25%`j3UI`0DaUr7l79`B1 zqqUARhW1^h6=)6?;@v>xrZNM;t}{yY3P@|L}ey@gG( z9r{}WoYN(9TW&dE2dEJIXkyHA4&pU6ki=rx&l2{DLGbVmg4%3Dlfvn!GB>EVaY_%3+Df{fBiqJV>~Xf8A0aqUjgpa} zoF8YXO&^_x*Ej}nw-$-F@(ddB>%RWoPUj?p8U{t0=n>gAI83y<9Ce@Q#3&(soJ{64 z37@Vij1}5fmzAuIUnXX`EYe;!H-yTVTmhAy;y8VZeB#vD{vw9~P#DiFiKQ|kWwGFZ z=jK;JX*A;Jr{#x?n8XUOLS;C%f|zj-7vXtlf_DtP7bpurBeX%Hjwr z4lI-2TdFpzkjgiv!8Vfv`=SP+s=^i3+N~1ELNWUbH|ytVu>EyPN_3(4TM^QE1swRo zoV7Y_g)a>28+hZG0e7g%@2^s>pzR4^fzR-El}ARTmtu!zjZLuX%>#OoU3}|rFjJg} zQ2TmaygxJ#sbHVyiA5KE+yH0LREWr%^C*yR|@gM$nK2P zo}M}PV0v))uJh&33N>#aU376@ZH79u(Yw`EQ2hM3SJs9f99+cO6_pNW$j$L-CtAfe zYfM)ccwD!P%LiBk!eCD?fHCGvgMQ%Q2oT_gmf?OY=A>&PaZQOq4eT=lwbaf}33LCH zFD|)lu{K7$8n9gX#w4~URjZxWm@wlH%oL#G|I~Fb-v^0L0TWu+`B+ZG!yII)w05DU z>GO?n(TN+B=>HdxVDSlIH76pta$_LhbBg;eZ`M7OGcqt||qi zogS72W1IN%=)5JCyOHWoFP7pOFK0L*OAh=i%&VW&4^LF@R;+K)t^S!96?}^+5QBIs zjJNTCh)?)4k^H^g1&jc>gysM`y^8Rm3qsvkr$9AeWwYpa$b22=yAd1t<*{ zaowSEFP+{y?Ob}8&cwfqoy4Pb9IA~VnM3u!trIK$&&0Op#Ql4j>(EW?UNUv#*iH1$ z^j>+W{afcd`{e&`-A{g}{JnIzYib)!T56IT@YEs{4|`sMpW3c8@UCoIJv`XsAw!XC z34|Il$LpW}CIHFC5e*)}00I5{%OL*WZRGzC0?_}-9{#ue?-ug^ zLE|uv-~6xnSs_2_&CN9{9vyc!Xgtn36_g^wI0C4s0s^;8+p?|mm;Odt3`2ZjwtK;l zfd6j)*Fr#53>C6Y8(N5?$H0ma;BCF3HCjUs7rpb2Kf*x3Xcj#O8mvs#&33i+McX zQpBxD8!O{5Y8D&0*QjD=Yhl9%M0)&_vk}bmN_Ud^BPN;H=U^bn&(csl-pkA+GyY0Z zKV7sU_4n;}uR78ouo8O%g*V;79KY?3d>k6%gpcmQsKk&@Vkw9yna_3asGt`0Hmj59 z%0yiF*`jXhByBI9QsD=+>big5{)BGe&+U2gAARGe3ID)xrid~QN_{I>k}@tzL!Md_ z&=7>TWciblF@EMC3t4-WX{?!m!G6$M$1S?NzF*2KHMP3Go4=#ZHkeIv{eEd;s-yD# z_jU^Ba06TZqvV|Yd;Z_sN%$X=!T+&?#p+OQIHS%!LO`Hx0q_Y0MyGYFNoM{W;&@0@ zLM^!X4KhdtsET5G<0+|q0oqVXMW~-7LW9Bg}=E$YtNh1#1D^6Mz(V9?2g~I1( zoz9Cz=8Hw98zVLwC2AQvp@pBeKyidn6Xu0-1SY1((^Hu*-!HxFUPs)yJ+i`^BC>PC zjwd0mygOVK#d2pRC9LxqGc6;Ui>f{YW9Bvb>33bp^NcnZoH~w9(lM5@JiIlfa-6|k ziy31UoMN%fvQfhi8^T+=yrP{QEyb-jK~>$A4SZT-N56NYEbpvO&yUme&pWKs3^94D zH{oXnUTb3T@H+RgzML*lejx`WAyw*?K7B-I(VJx($2!NXYm%3`=F~TbLv3H<{>D?A zJo-FDYdSA-(Y%;4KUP2SpHKAIcv9-ld(UEJE7=TKp|Gryn;72?0LHqAN^fk6%8PCW z{g_-t)G5uCIf0I`*F0ZNl)Z>))MaLMpXgqWgj-y;R+@A+AzDjsTqw2Mo9ULKA3c70 z!7SOkMtZb+MStH>9MnvNV0G;pwSW9HgP+`tg}e{ij0H6Zt5zJ7iw`hEnvye!XbA@!~#%vIkzowCOvq5I5@$3wtc*w2R$7!$*?}vg4;eDyJ_1=ixJuEp3pUS27W?qq(P^8$_lU!mRChT}ctvZz4p!X^ zOSp|JOAi~f?UkwH#9k{0smZ7-#=lK6X3OFEMl7%)WIcHb=#ZN$L=aD`#DZKOG4p4r zwlQ~XDZ`R-RbF&hZZhu3(67kggsM-F4Y_tI^PH8PMJRcs7NS9ogF+?bZB*fcpJ z=LTM4W=N9yepVvTj&Hu~0?*vR1HgtEvf8w%Q;U0^`2@e8{SwgX5d(cQ|1(!|i$km! zvY03MK}j`sff;*-%mN~ST>xU$6Bu?*Hm%l@0dk;j@%>}jsgDcQ)Hn*UfuThz9(ww_ zasV`rSrp_^bp-0sx>i35FzJwA!d6cZ5#5#nr@GcPEjNnFHIrtUYm1^Z$;{d&{hQV9 z6EfFHaIS}46p^5I-D_EcwwzUUuO}mqRh&T7r9sfw`)G^Q%oHxEs~+XoM?8e*{-&!7 z7$m$lg9t9KP9282eke608^Q2E%H-xm|oJ8=*SyEo} z@&;TQ3K)jgspgKHyGiKVMCz>xmC=H5Fy3!=TP)-R3|&1S-B)!6q50wfLHKM@7Bq6E z44CY%G;GY>tC`~yh!qv~YdXw! zSkquvYNs6k1r7>Eza?Vkkxo6XRS$W7EzL&A`o>=$HXgBp{L(i^$}t`NcnAxzbH8Ht z2!;`bhKIh`f1hIFcI5bHI=ueKdzmB9)!z$s-BT4ItyY|NaA_+o=jO%MU5as9 zc2)aLP>N%u>wlaXTK!p)r?+~)L+0eCGb5{8WIk7K52$nufnQ+m8YF+GQc&{^(zh-$ z#wyWV*Zh@d!b(WwXqvfhQX)^aoHTBkc;4ossV3&Ut*k>AI|m+{#kh4B!`3*<)EJVj zwrxK>99v^k4&Y&`Awm>|exo}NvewV%E+@vOc>5>%H#BK9uaE2$vje zWYM5fKuOTtn96B_2~~!xJPIcXF>E_;yO8AwpJ4)V`Hht#wbO3Ung~@c%%=FX4)q+9 z99#>VC2!4l`~0WHs9FI$Nz+abUq# zz`Of97})Su=^rGp2S$)7N3rQCj#0%2YO<R&p>$<#lgXcUj=4H_{oAYiT3 z44*xDn-$wEzRw7#@6aD)EGO$0{!C5Z^7#yl1o;k0PhN=aVUQu~eTQ^Xy{z8Ow6tk83 z4{5xe%(hx)%nD&|e*6sTWH`4W&U!Jae#U4TnICheJmsw{l|CH?UA{a6?2GNgpZLyzU2UlFu1ZVwlALmh_DOs03J^Cjh1im`E3?9&zvNmg(MuMw&0^Lu$(#CJ*q6DjlKsY-RMJ^8yIY|{SQZ*9~CH|u9L z`R78^r=EbbR*_>5?-)I+$6i}G)%mN(`!X72KaV(MNUP7Nv3MS9S|Pe!%N2AeOt5zG zVJ;jI4HZ$W->Ai_4X+`9c(~m=@ek*m`ZQbv3ryI-AD#AH=`x$~WeW~M{Js57(K7(v ze5`};LG|%C_tmd>bkufMWmAo&B+DT9ZV~h(4jg0>^aeAqL`PEUzJJtI8W1M!bQWpv zvN(d}E1@nlYa!L!!A*RN!(Q3F%J?5PvQ0udu?q-T)j3JKV~NL>KRb~w-lWc685uS6 z=S#aR&B8Sc8>cGJ!!--?kwsJTUUm`Jk?7`H z7PrO~xgBrSW2_tTlCq1LH8*!o?pj?qxy8}(=r_;G18POrFh#;buWR0qU24+XUaVZ0 z?(sXcr@-YqvkCmHr{U2oPogHL{r#3r49TeR<{SJX1pcUqyWPrkYz^X8#QW~?F)R5i z>p^!i<;qM8Nf{-fd6!_&V*e_9qP6q(s<--&1Ttj01j0w>bXY7y1W*%Auu&p|XSOH=)V7Bd4fUKh&T1)@cvqhuD-d=?w}O zjI%i(f|thk0Go*!d7D%0^ztBfE*V=(ZIN84f5HU}T9?ulmEYzT5usi=DeuI*d|;M~ zp_=Cx^!4k#=m_qSPBr5EK~E?3J{dWWPH&oCcNepYVqL?nh4D5ynfWip$m*YlZ8r^Z zuFEUL-nW!3qjRCLIWPT0x)FDL7>Yt7@8dA?R2kF@WE>ysMY+)lTsgNM#3VbXVGL}F z1O(>q>2a+_`6r5Xv$NZAnp=Kgnr3)cL(^=8ypEeOf3q8(HGe@7Tt59;yFl||w|mnO zHDxg2G3z8=(6wjj9kbcEY@Z0iOd7Gq5GiPS5% z*sF1J<#daxDV2Z8H>wxOF<;yKzMeTaSOp_|XkS9Sfn6Mpe9UBi1cSTieGG5$O;ZLIIJ60Y>SN4vC?=yE_CWlo(EEE$e4j?z&^FM%kNmRtlbEL^dPPgvs9sbK5fGw*r@ z+!EU@u$T8!nZh?Fdf_qk$VuHk^yVw`h`_#KoS*N%epIIOfQUy_&V}VWDGp3tplMbf z5Se1sJUC$7N0F1-9jdV2mmGK{-}fu|Nv;12jDy0<-kf^AmkDnu6j~TPWOgy1MT68|D z=4=50jVbUKdKaQgD`eWGr3I&^<6uhkjz$YwItY8%Yp9{z4-{6g{73<_b*@XJ4Nm3-3z z?BW3{aY_ccRjb@W1)i5nLg|7BnWS!B`_Uo9CWaE`Ij327QH?i)9A}4Ug4wmxVVa^b z-4+m%-wwOl7cKH7+=x&nrCrbEC)Q$fpg&V83#uEH;C=GNMz`ps@^RxK%T*8%OPnC` z{WO~J%nxYJ`x|N%?&i7?;{_8t^jM&=50HlaOQj8fS}_`moH$c;vI<|cruPFnpT8yU zS%rPOCUSd5Zdb(zwk`hqwTQn)*&n)uYsP*F_(~xEWq}C= zv30kFmZFwJZ@ELVX3?$dXQh|icO7UrL*_5G=I^xXjImz`ZPp>?g#tf(ej~KaIU0algsG!IS09;>?MvqGg#c{i+}qY|{P8W~O%#>|gFd z<1dr$-oxyRGN17yZo1OwLnzwYs0|;IS_nymNB0IlSzPQ%-r`?T=;_XQ^~&#}b|AB} zkNbN5uB?-sUB-T5QLlg%Uk3)uHB;>VIzGe9_J9 zaeISkQm!v(9d(0ML^b9fR^sfHFlH?7Mvddt37OuR{|O0{uv)(&-6<87W4 zyO>s!=cPgP3O&7xxU5DlIPw_o3O>6o6Qb?JWs3qw#p3sBc3g$?Dx zi(6D+DYgV;GrUis-CL%Qe{nvZnwaVXmbhH(|GFh|Q)k=1uvA$I@1DXI7bKlQ@8D6P zS?(*?><>)G49q0wr;NajpxP4W2G)kHl6^=Z>hrNEI4Mwd_$O6$1dXF;Q#hE(-eeW6 zz03GJF%Wl?HO=_ztv5*zRlcU~{+{k%#N59mgm~eK>P!QZ6E?#Cu^2)+K8m@ySvZ*5 z|HDT}BkF@3!l(0%75G=1u2hETXEj!^1Z$!)!lyGXlWD!_vqGE$Z)#cUVBqlORW>0^ zDjyVTxwKHKG|0}j-`;!R-p>}qQfBl(?($7pP<+Y8QE#M8SCDq~k<+>Q^Zf@cT_WdX3~BSe z+|KK|7OL5Hm5(NFP~j>Ct3*$wi0n0!xl=(C61`q&cec@mFlH(sy%+RH<=s)8aAPN`SfJdkAQjdv82G5iRdv8 zh{9wHUZaniSEpslXl^_ODh}mypC?b*9FzLjb~H@3DFSe;D(A-K3t3eOTB(m~I6C;(-lKAvit(70k`%@+O*Ztdz;}|_TS~B?Tpmi=QKC^m_ z2YpEaT3iiz*;T~ap1yiA)a`dKMwu`^UhIUeltNQ1Yjo=q@bI@&3zH?rVUg=IxLy-ni zyxDu%-Fr{H6owTjZU2O5>nDb=q&Jz_TjeSq%!2m40x&U6w~GQ({quPL73IsJS;f`$ zsuhioqCBj(gJ>2hoo)Gou7(WP*pX)f=Y=!=k!&1K?EYY%jJ~X&DnK{^saPQK<1BJ z_A`_{%ZozcB(3w$z^To^6d|XuT@=X~wtW!+{4ID@N{AB~J6AL5vuY>JwvWCNFKsKh zd}@>q@_WV#QZ&UJ0#?X(pXR!oyXOEG3rqzHbCzGLONDb042i$})fM@XF)uSP(DHUc z^&{|$*xe{cs?Gp8=B%RY3L7#$ve$?TWh>MZdxF1zH1v}1z+$Ov#G7?%D)bBCyDe*% zSeKSpETC2V1){II>@UwJi>4uBN+iAx+82E~gb|Cr&8E^i&)A!uv-g?jzH99wU}8+# z$nh>yvb;TwZmS@7LrvuCu_d0-WxFNI&C7%sWuTL%YU!l|I1{|->=dlOeHOCtUO#zkS3ESO8LHV4hTdQL5EdV zuWD33fFPH}HPrW^s$Qn1Xgp&AT6<-He{{4%eIu3rN=iK|9mURdKXfB&Q?qGok%!cs ze53UP{Z!TO-Y@q2;;k2avA3`lm4OoN4@S*k=UA)7H;qZ`d8`XaYFCv?Ba+uGW@r5v z&&{nf(24WSBOhc7!qF^@0cz;XcUynNaj6w2349;s!K{KVqs5yS{ z7VubS`2OzT^5#1~6Tt^RTvt9-J|D2F>y~>2;jeF>g`hx5l%B3H=aLExQihuYngzlnBTYOTHJQMzl>kwqN5JYs)Ej zblA@ntkUS~xi+}y6|(81helS}Q~&VB37qyV|S3Y=><^1wh%msQM?fz z<58MX(=|PSUKCF#)dbhR%D&xgCD?$aR0qen+wpp6 zst}vX18!Be96TD??j1HsHTUx(a&@F?=gT`Q$oJFFyrh^;zgz!(NlAHGn0cJy@us=w zNhC#l5G;H}+>49Nsh12=ZPO2r*2OBQe5kpb&1?*PIBFitK8}FUfb~S-#hKfF0o#&d z#3aPkB$9scYku&kA6{0xHnBV#&Wei5J>5T-XX-gUXEPo+9b7WL=*XESc(3BshL`aj zXp}QIp*40}oWJt*l043e8_5;H5PI5c)U&IEw5dF(4zjX0y_lk9 zAp@!mK>WUqHo)-jop=DoK>&no>kAD=^qIE7qis&_*4~ z6q^EF$D@R~3_xseCG>Ikb6Gfofb$g|75PPyyZN&tiRxqovo_k zO|HA|sgy#B<32gyU9x^&)H$1jvw@qp+1b(eGAb)O%O!&pyX@^nQd^9BQ4{(F8<}|A zhF&)xusQhtoXOOhic=8#Xtt5&slLia3c*a?dIeczyTbC#>FTfiLST57nc3@Y#v_Eg#VUv zT8cKH#f3=1PNj!Oroz_MAR*pow%Y0*6YCYmUy^7`^r|j23Q~^*TW#cU7CHf0eAD_0 zEWEVddxFgQ7=!nEBQ|ibaScslvhuUk^*%b#QUNrEB{3PG@uTxNwW}Bs4$nS9wc(~O zG7Iq>aMsYkcr!9#A;HNsJrwTDYkK8ikdj{M;N$sN6BqJ<8~z>T20{J8Z2rRUuH7~3 z=tgS`AgxbBOMg87UT4Lwge`*Y=01Dvk>)^{Iu+n6fuVX4%}>?3czOGR$0 zpp*wp>bsFFSV`V;r_m+TZns$ZprIi`OUMhe^cLE$2O+pP3nP!YB$ry}2THx2QJs3< za1;>d-AggCarrQ>&Z!d@;mW+!q6eXhb&`GbzUDSxpl8AJ#Cm#tuc)_xh(2NV=5XMs zrf_ozRYO$NkC=pKFX5OH8v1>0i9Z$ec`~Mf+_jQ68spn(CJwclDhEEkH2Qw;${J$clv__nUjn5jA0wCLEnu1j;v!0vB>Ri6m9`;R{JMS%^)4FC zU0Z44+u$I$w=Bj|iu4DT5h~sS`C*zbmX?@-crY}E+hy>}2~C0Nn(EKk@5^qO4@l@! z6O0lr%tzGC`D^)8xU3FnMZVm0kX1sBWhaQyzVoXFWwr%Ny?=2M{5s#5i7fTu3gEkG zc{(Pr$v=;`Y#&`y*J}#M9ux>0?xu!`$9cUKm#Bdd_&S#LPTS?ZPV6zN6>W6JTS~-LfjL{mB=b(KMk3 z2HjBSlJeyUVqDd=Mt!=hpYsvby2GL&3~zm;0{^nZJq+4vb?5HH4wufvr}IX42sHeK zm@x?HN$8TsTavXs)tLDFJtY9b)y~Tl@7z4^I8oUQq4JckH@~CVQ;FoK(+e0XAM>1O z(ei}h?)JQp>)d=6ng-BZF1Z5hsAKW@mXq+hU?r8I(*%`tnIIOXw7V6ZK(T9RFJJe@ zZS!aC+p)Gf2Ujc=a6hx4!A1Th%YH!Lb^xpI!Eu` zmJO{9rw){B1Ql18d%F%da+Tbu1()?o(zT7StYqK6_w`e+fjXq5L^y(0 z09QA6H4oFj59c2wR~{~>jUoDzDdKz}5#onYPJRwa`SUO)Pd4)?(ENBaFVLJr6Kvz= zhTtXqbx09C1z~~iZt;g^9_2nCZ{};-b4dQJbv8HsWHXPVg^@(*!@xycp#R?a|L!+` zY5w))JWV`Gls(=}shH0#r*;~>_+-P5Qc978+QUd>J%`fyn{*TsiG-dWMiJXNgwBaT zJ=wgYFt+1ACW)XwtNx)Q9tA2LPoB&DkL16P)ERWQlY4%Y`-5aM9mZ{eKPUgI!~J3Z zkMd5A_p&v?V-o-6TUa8BndiX?ooviev(DKw=*bBVOW|=zps9=Yl|-R5@yJe*BPzN}a0mUsLn{4LfjB_oxpv(mwq# zSY*%E{iB)sNvWfzg-B!R!|+x(Q|b@>{-~cFvdDHA{F2sFGA5QGiIWy#3?P2JIpPKg6ncI^)dvqe`_|N=8 '} + 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..7ce2a17c --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java @@ -0,0 +1,109 @@ +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.fml.config.ModConfig; +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) { + modContainer.registerConfig(ModConfig.Type.COMMON, Config.SPEC); + + // 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/Config.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Config.java new file mode 100644 index 00000000..69a2bb81 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Config.java @@ -0,0 +1,42 @@ +package com.box3lab.box3js; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.fml.event.config.ModConfigEvent; +import net.neoforged.neoforge.common.ModConfigSpec; + +// An example config class. This is not required, but it's a good idea to have one to keep your config organized. +// Demonstrates how to use Neo's config APIs +public class Config { + private static final ModConfigSpec.Builder BUILDER = new ModConfigSpec.Builder(); + + public static final ModConfigSpec.BooleanValue LOG_DIRT_BLOCK = BUILDER + .comment("Whether to log the dirt block on common setup") + .define("logDirtBlock", true); + + public static final ModConfigSpec.IntValue MAGIC_NUMBER = BUILDER + .comment("A magic number") + .defineInRange("magicNumber", 42, 0, Integer.MAX_VALUE); + + public static final ModConfigSpec.ConfigValue MAGIC_NUMBER_INTRODUCTION = BUILDER + .comment("What you want the introduction message to be for the magic number") + .define("magicNumberIntroduction", "The magic number is... "); + + // a list of strings that are treated as resource locations for items + public static final ModConfigSpec.ConfigValue> ITEM_STRINGS = BUILDER + .comment("A list of items to log on common setup.") + .defineListAllowEmpty("items", List.of("minecraft:iron_ingot"), () -> "", Config::validateItemName); + + static final ModConfigSpec SPEC = BUILDER.build(); + + private static boolean validateItemName(final Object obj) { + return obj instanceof String itemName && BuiltInRegistries.ITEM.containsKey(ResourceLocation.parse(itemName)); + } +} 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..2eaba2eb --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEntity.java @@ -0,0 +1,334 @@ +package com.box3lab.box3js.script; + +import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundSource; +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.UUID; +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) { + setProp("meshInvisible", v); + entity.setInvisible(v); + } + + // ---- Tags ---- + + public void addTag(String tag) { + entity.addTag(tag); + } + + public boolean hasTag(String tag) { + return entity.getTags().contains(tag); + } + + public void removeTag(String tag) { + entity.removeTag(tag); + } + + // ---- Sound ---- + + public void sound(String path) { + if (entity instanceof ServerPlayer sp) { + sp.playNotifySound( + net.minecraft.sounds.SoundEvents.NOTE_BLOCK_PLING.value(), + SoundSource.PLAYERS, 1.0f, 1.0f); + } + } + + // ---- Glowing (MC extension) ---- + + public boolean isGlowing() { return entity.isCurrentlyGlowing(); } + public void setGlowing(boolean v) { entity.setGlowingTag(v); } + + // ---- Name tag (MC extension) ---- + + public String getNameTag() { + var cn = entity.getCustomName(); + return cn != null ? cn.getString() : ""; + } + + public void setNameTag(String name) { + 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() { + if (entity instanceof LivingEntity le) return le.getHealth(); + return getProp("hp", 100.0); + } + public void setHp(double v) { + setProp("hp", v); + if (entity instanceof LivingEntity le) { + double max = le.getMaxHealth(); + le.setHealth((float) Math.max(0, Math.min(v, max))); + } + } + + public double getMaxHp() { + if (entity instanceof LivingEntity le) return le.getMaxHealth(); + return getProp("maxHp", 100.0); + } + public void setMaxHp(double v) { + setProp("maxHp", v); + if (entity instanceof LivingEntity le) { + 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) { + if (entity instanceof LivingEntity le) { + le.hurt(le.damageSources().generic(), (float) amount); + } + } + + public void heal(double amount) { + if (entity instanceof LivingEntity le) { + le.heal((float) amount); + } + } + + // ---- Invulnerable (MC extension) ---- + + public boolean isInvulnerable() { return entity.isInvulnerable(); } + public void setInvulnerable(boolean v) { entity.setInvulnerable(v); } + + // ---- Fire (MC extension) ---- + + public void setFire(int ticks) { + entity.setRemainingFireTicks(ticks); + } + + public void clearFire() { + entity.setRemainingFireTicks(0); + } + + // ---- Equipment & Effects (MC extension) ---- + + /** Set equipment slot for a mob. slot: mainhand/offhand/head/chest/legs/feet */ + public void setEquipment(String slot, String itemId) { + if (!(entity instanceof Mob mob)) return; + EquipmentSlot equipmentSlot = 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; + }; + if (equipmentSlot == null) return; + ResourceLocation rl = ResourceLocation.tryParse(itemId); + if (rl == null) return; + Item item = BuiltInRegistries.ITEM.getOptional(rl).orElse(null); + if (item == null) return; + mob.setItemSlot(equipmentSlot, new ItemStack(item)); + } + + /** Add a potion effect to a LivingEntity. Works on all entities, not just players. */ + public void addEffect(String effectId, int duration, int amplifier) { + if (!(entity instanceof LivingEntity le)) return; + ResourceLocation rl = ResourceLocation.tryParse(effectId); + if (rl == null) return; + Holder effect = BuiltInRegistries.MOB_EFFECT.getHolder(rl).orElse(null); + if (effect == null) return; + le.addEffect(new MobEffectInstance(effect, duration, amplifier)); + } + + // ---- Look at (MC extension) ---- + + public void lookAt(double x, double y, double z) { + double dx = x - entity.getX(); + double dy = y - entity.getEyeY(); + double dz = z - entity.getZ(); + double horizontalDist = Math.sqrt(dx * dx + dz * dz); + float yaw = (float) (Math.toDegrees(Math.atan2(dz, dx)) - 90.0); + float pitch = (float) (-Math.toDegrees(Math.atan2(dy, horizontalDist))); + entity.setYRot(yaw); + entity.setXRot(pitch); + } + + // ---- Navigation (MC extension) ---- + + /** Pathfinding-based movement to a target position. Returns true if a path was found. */ + 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; + } + + /** Set the mob's attack target. The mob will pathfind to and attack the target. */ + public void setTarget(Box3JSEntity target) { + 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() { + 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) { + if (entity instanceof Mob mob) { + mob.setNoAi(!enabled); + } + } + + // ---- 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); + } + + /** 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/Box3JSPlayer.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java new file mode 100644 index 00000000..a25a7b14 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java @@ -0,0 +1,369 @@ +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 java.util.LinkedHashMap; +import java.util.Map; + +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(); } + + // ---- Appearance ---- + + public boolean getInvisible() { return player.isInvisible(); } + public void setInvisible(boolean v) { player.setInvisible(v); } + + public double getScale() { return player.getScale(); } + + // ---- Movement ---- + + public double getWalkSpeed() { return player.getAttributeValue(Attributes.MOVEMENT_SPEED); } + public void setWalkSpeed(double v) { + player.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(v); + } + + public double getRunSpeed() { return player.getAttributeValue(Attributes.MOVEMENT_SPEED) * 1.3; } + public void setRunSpeed(double v) { + player.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(v / 1.3); + } + + public double getJumpPower() { return player.getAttributeValue(Attributes.JUMP_STRENGTH); } + public void setJumpPower(double v) { + 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) { + player.getAbilities().mayfly = v; + player.onUpdateAbilities(); + } + + public boolean getSpectator() { return player.isSpectator(); } + + public double getFlySpeed() { return player.getAbilities().getFlyingSpeed(); } + public void setFlySpeed(double v) { + player.getAbilities().setFlyingSpeed((float) v); + player.onUpdateAbilities(); + } + + // ---- Game Mode ---- + + public String getGameMode() { return player.gameMode.getGameModeForPlayer().getName(); } + public void setGameMode(Object v) { + 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) { + 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)); + + Map result = new LinkedHashMap<>(); + result.put("index", 0); + result.put("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); + } + + // ---- Sound ---- + + public void sound(String path) { + player.playNotifySound( + net.minecraft.sounds.SoundEvents.NOTE_BLOCK_PLING.value(), + net.minecraft.sounds.SoundSource.PLAYERS, 1.0f, 1.0f); + } + + public void playSound(String path, float volume, float pitch) { + ResourceLocation rl = ResourceLocation.tryParse(path); + if (rl == null) return; + var sound = net.minecraft.core.registries.BuiltInRegistries.SOUND_EVENT.getOptional(rl); + if (sound.isPresent()) { + player.playNotifySound(sound.get(), net.minecraft.sounds.SoundSource.PLAYERS, volume, pitch); + } + } + + // ---- Look at (MC extension) ---- + + public void lookAt(double x, double y, double z) { + double dx = x - player.getX(); + double dy = y - player.getEyeY(); + double dz = z - player.getZ(); + double hd = Math.sqrt(dx * dx + dz * dz); + player.setYRot((float) (Math.toDegrees(Math.atan2(dz, dx)) - 90.0)); + player.setXRot((float) (-Math.toDegrees(Math.atan2(dy, hd)))); + } + + // ---- Command ---- + + public void runCommand(String cmd) { + net.minecraft.commands.CommandSourceStack source = player.createCommandSourceStack(); + server.getCommands().performPrefixedCommand(source, cmd); + } + + // ---- Inventory ---- + + public void giveItem(String itemId, int count) { + ResourceLocation rl = ResourceLocation.tryParse(itemId); + if (rl == null) return; + var item = BuiltInRegistries.ITEM.getOptional(rl); + if (item.isPresent()) { + ItemStack stack = new ItemStack(item.get(), Math.max(1, Math.min(count, 64))); + player.getInventory().add(stack); + } + } + + public void clearInventory() { + player.getInventory().clearContent(); + } + + public Object getHeldItem() { + ItemStack stack = player.getMainHandItem(); + Map result = new LinkedHashMap<>(); + if (stack.isEmpty()) { + result.put("id", "minecraft:air"); + result.put("count", 0); + return result; + } + ResourceLocation key = BuiltInRegistries.ITEM.getKey(stack.getItem()); + result.put("id", key.toString()); + result.put("count", stack.getCount()); + return result; + } + + // ---- Effects ---- + + public void addEffect(String effectId, int duration, int amplifier) { + ResourceLocation rl = ResourceLocation.tryParse(effectId); + if (rl == null) return; + var effect = BuiltInRegistries.MOB_EFFECT.getHolder(rl); + if (effect.isPresent()) { + player.addEffect(new MobEffectInstance(effect.get(), duration, amplifier)); + } + } + + public void clearEffects() { + player.removeAllEffects(); + } + + // ---- XP / Food ---- + + public int getXp() { return player.experienceLevel; } + public void setXp(int v) { player.experienceLevel = v; } + + 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); } + + // ---- Custom properties ---- + + 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); + } +} 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..6ea47b70 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSStorage.java @@ -0,0 +1,320 @@ +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.*; + +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; + + public Box3JSStorage(Path configDir, Box3ScriptEngine engine) { + this.baseDir = configDir.resolve("box3").resolve("storage"); + this.engine = engine; + try { Files.createDirectories(baseDir); } catch (IOException ignored) {} + } + + // ---- GameStorage ---- + + /** storage.key — always empty for MC local storage */ + public String getKey() { return ""; } + + /** storage.getDataStorage(name): GameDataStorage */ + public GameDataStorage getDataStorage(String name) { + return new GameDataStorage(name); + } + + /** storage.getGroupStorage(name): GameDataStorage — same as getDataStorage in MC */ + public GameDataStorage getGroupStorage(String name) { + return new GameDataStorage(name); + } + + // ---- ValueEntry (internal metadata container) ---- + + 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 (JS-accessible) ---- + + 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 (JS-accessible) ---- + + 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]; + List slice = all.subList(cursor, end); + return slice.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; + + 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"); + } + + private String sanitize(String s) { + return s.replaceAll("[^a-zA-Z0-9_.\\-]", "_"); + } + + /** GameDataStorage.key — read-only space name */ + public String getKey() { return name; } + + // ---- Internal read/write ---- + + private synchronized Map read() { + if (!Files.exists(path)) return new LinkedHashMap<>(); + try { + String json = Files.readString(path); + Map map = GSON.fromJson(json, MAP_TYPE); + return map != null ? new LinkedHashMap<>(map) : new LinkedHashMap<>(); + } catch (IOException e) { + return new LinkedHashMap<>(); + } + } + + private synchronized void write(Map map) { + try { + Files.createDirectories(path.getParent()); + Files.writeString(path, GSON.toJson(map)); + } catch (IOException ignored) {} + } + + // ---- Public API ---- + + /** set(key: string, value: JSONValue): void */ + public void set(String key, Object value) { + if (key == null) return; + Map map = read(); + ValueEntry existing = map.get(key); + long now = System.currentTimeMillis(); + if (existing != null) { + existing.value = value; + existing.updateTime = now; + existing.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); + } else { + map.put(key, new ValueEntry(value, now)); + } + write(map); + } + + /** get(key: string): value — returns the stored value directly */ + public Object get(String key) { + if (key == null) return null; + Map map = read(); + ValueEntry entry = map.get(key); + return entry != null ? entry.value : null; + } + + /** update(key: string, handler: function(prevValue): value): void */ + public void update(String key, Function handler) { + if (key == null || handler == null) return; + Map map = read(); + ValueEntry entry = map.get(key); + if (entry == null) return; // can't update non-existent key per Box3 spec + Object newValue = engine.callFunction(handler, entry.value); + long now = System.currentTimeMillis(); + entry.value = newValue; + entry.updateTime = now; + entry.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); + write(map); + } + + /** remove(key: string): value — returns the old value */ + public Object remove(String key) { + if (key == null) return null; + Map map = read(); + ValueEntry entry = map.remove(key); + if (entry != null) { + write(map); + return entry.value; + } + return null; + } + + /** increment(key: string, value?: number): number — atomic increment, default delta=1 */ + public double increment(String key, double value) { + if (key == null) return 0; + // Rhino calls increment(key) with undefined for the second arg, + // which maps to Double.NaN in Java. Handle that case. + double delta = Double.isNaN(value) ? 1.0 : value; + synchronized (this) { + Map map = read(); + ValueEntry entry = map.get(key); + long now = System.currentTimeMillis(); + 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); + map.put(key, entry); + } + write(map); + return ((Number) entry.value).doubleValue(); + } + } + + // Overload for Rhino: when called with 1 arg + public double increment(String key) { + return increment(key, 1.0); + } + + /** list(options: {cursor, pageSize?, ascending?, max?, min?, constraintTarget?}): QueryList */ + public QueryList list(Map options) { + Map map = read(); + List results = new ArrayList<>(); + + for (Map.Entry e : map.entrySet()) { + results.add(new ReturnValue(e.getKey(), e.getValue())); + } + + // Parse options + 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; + + // Sort + 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; + }); + } + + // Filter by min/max + 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; + } + // Navigate nested path like "a.b.c" + 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; + } + + /** destroy(): void — delete this data storage space */ + public void destroy() { + try { Files.deleteIfExists(path); } catch (IOException ignored) {} + synchronized (this) { + try { Files.deleteIfExists(path); } catch (IOException ignored) {} + } + } + } +} 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..08b847dd --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSVoxels.java @@ -0,0 +1,313 @@ +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 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) { + this.server = server; + + 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 + ResourceLocation rl = ResourceLocation.tryParse(name); + if (rl != null) { + Block block = BuiltInRegistries.BLOCK.getOptional(rl).orElse(null); + 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(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 (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; + } + + /** 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); + } + } + } + } + + /** 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; + } + + /** 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); + + 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; + } + + // ---- 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; + } + + /** 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; + } + + /** 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(); + } + + /** 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); + } + + /** 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") + ResourceLocation rl = ResourceLocation.tryParse(s); + if (rl == null) return null; + return BuiltInRegistries.BLOCK.getOptional(rl).orElse(null); + } + 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..ec982cf4 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSWorld.java @@ -0,0 +1,739 @@ +package com.box3lab.box3js.script; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.network.chat.Component; +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.server.level.ServerPlayer; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.Difficulty; +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 net.minecraft.world.scores.PlayerTeam; +import net.minecraft.ChatFormatting; +import net.minecraft.server.level.ServerBossEvent; +import net.minecraft.world.BossEvent.BossBarColor; +import net.minecraft.world.BossEvent.BossBarOverlay; +import net.minecraft.core.component.DataComponents; +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.border.WorldBorder; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.storage.ServerLevelData; +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 org.mozilla.javascript.Function; + +import java.util.*; + +public class Box3JSWorld { + + private final MinecraftServer server; + private final Box3ScriptEngine engine; + private final Map bossBars = new HashMap<>(); + + public Box3JSWorld(MinecraftServer server, Box3ScriptEngine engine) { + this.server = server; + this.engine = engine; + } + + // ---- Namespace fields ---- + public final ScoreboardNS scoreboard = new ScoreboardNS(); + public final BossBarNS bossbar = new BossBarNS(); + public final TeamNS team = new TeamNS(); + public final BorderNS border = new BorderNS(); + public final LightningNS lightning = new LightningNS(); + public final FireworkNS firework = new FireworkNS(); + public final ParticleNS particle = new ParticleNS(); + public final DropNS drop = new DropNS(); + + // ---- 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) { + server.overworld().getLevelData().setRaining(v > 0); + } + + public double getThunderDensity() { + return server.overworld().getThunderLevel(1.0f); + } + public void setThunderDensity(double v) { + ((ServerLevelData) server.overworld().getLevelData()).setThundering(v > 0); + } + + public void clearWeather() { + 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) { server.overworld().setDayTime(tick); } + + public double getTimeScale() { + return server.overworld().getGameRules().getBoolean(GameRules.RULE_DAYLIGHT) ? 1.0 : 0.0; + } + public void setTimeScale(double v) { + 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) { + Difficulty diff; + if (v instanceof Number n) { + diff = Difficulty.byId(n.intValue()); + } else { + diff = Difficulty.byName(v.toString()); + } + if (diff != null) server.setDifficulty(diff, true); + } + + // ---- 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) { + ResourceLocation rl = ResourceLocation.tryParse(type); + if (rl == null) return null; + var opt = BuiltInRegistries.ENTITY_TYPE.getOptional(rl); + if (opt.isEmpty()) return null; + Entity entity = opt.get().create(server.overworld()); + if (entity == null) return null; + entity.setPos(pos.x, pos.y, pos.z); + server.overworld().addFreshEntity(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)); + } + + // ---- Entity Query ---- + + public 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; + } + + public Box3JSEntity querySelector(String selector) { + List all = querySelectorAll(selector); + return all.isEmpty() ? null : all.get(0); + } + + private boolean matchesSelector(Box3JSEntity entity, String selector) { + if (selector.equals("*") || selector.equals("player")) return entity.isPlayer(); + if (selector.startsWith("#")) { + String id = selector.substring(1); + return id.equals(entity.getId()); + } + if (selector.startsWith(".")) { + String tag = selector.substring(1); + return entity.hasTag(tag); + } + return false; + } + + // ---- 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) { + CommandSourceStack source = server.createCommandSourceStack(); + server.getCommands().performPrefixedCommand(source, cmd); + } + + private 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; + } + + // ---- Entity query (MC extension) ---- + + public List getEntitiesInArea(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, e -> true)) { + result.add(new Box3JSEntity(e, server, engine)); + } + return result; + } + + // ---- Raycast (MC extension) ---- + + public Object raycast(GameVector3 origin, GameVector3 direction) { + return raycast(origin, direction, 5.0); + } + + public 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) { + Map result = new LinkedHashMap<>(); + result.put("hit", false); + return result; + } + Vec3 dir = new Vec3(direction.x / len, direction.y / len, direction.z / len); + Vec3 end = start.add(dir.scale(maxDistance)); + + // Block raycast + ClipContext ctx = new ClipContext(start, end, ClipContext.Block.OUTLINE, ClipContext.Fluid.NONE, CollisionContext.empty()); + BlockHitResult blockHit = level.clip(ctx); + + // Entity raycast + 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, e -> 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; + + Map result = new LinkedHashMap<>(); + if (closestEntity != null && closestEntDistSqr < blockDistSqr) { + result.put("hit", true); + result.put("x", entityHitPos.x); + result.put("y", entityHitPos.y); + result.put("z", entityHitPos.z); + result.put("normalX", 0); + result.put("normalY", 0); + result.put("normalZ", 0); + result.put("distance", Math.sqrt(closestEntDistSqr)); + result.put("entity", new Box3JSEntity(closestEntity, server, engine)); + } else if (blockHit.getType() != HitResult.Type.MISS) { + Vec3 pos = blockHit.getLocation(); + result.put("hit", true); + result.put("x", pos.x); + result.put("y", pos.y); + result.put("z", pos.z); + Direction face = blockHit.getDirection(); + result.put("normalX", face.getStepX()); + result.put("normalY", face.getStepY()); + result.put("normalZ", face.getStepZ()); + result.put("distance", Math.sqrt(blockDistSqr)); + result.put("entity", null); + BlockPos bp = blockHit.getBlockPos(); + result.put("voxel", engine.getVoxelsBinding().getId(level.getBlockState(bp))); + } else { + result.put("hit", false); + } + return result; + } + + // ---- Explosion (MC extension) ---- + + public void explode(double x, double y, double z, float power) { + explode(x, y, z, power, false); + } + + public void explode(double x, double y, double z, float power, boolean fire) { + server.overworld().explode(null, x, y, z, power, fire, Level.ExplosionInteraction.BLOCK); + } + + // ---- Sound / Biome (MC extension) ---- + + public void playSoundAll(String path, double x, double y, double z, float volume, float pitch) { + ResourceLocation rl = ResourceLocation.tryParse(path); + if (rl == null) return; + var sound = BuiltInRegistries.SOUND_EVENT.getHolder(rl); + if (sound.isEmpty()) return; + var packet = new ClientboundSoundPacket(sound.get(), SoundSource.PLAYERS, x, y, z, volume, pitch, server.overworld().getRandom().nextLong()); + for (ServerPlayer sp : server.getPlayerList().getPlayers()) { + sp.connection.send(packet); + } + } + + public 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"); + } + + // ---- Callback interfaces ---- + + @FunctionalInterface + public interface PlayerJoinCallback { + void onJoin(Box3JSEntity entity); + } + + @FunctionalInterface + public interface PlayerLeaveCallback { + void onLeave(Box3JSEntity entity); + } + + @FunctionalInterface + public interface VoxelDestroyCallback { + void onDestroy(Box3JSEntity entity, int x, int y, int z, String voxel, long tick); + } + + @FunctionalInterface + public interface VoxelContactCallback { + void onContact(Box3JSEntity entity, int voxel, int x, int y, int z, int axis, double force, long tick); + } + + @FunctionalInterface + public interface InteractCallback { + void onInteract(Box3JSEntity entity, Box3JSEntity target, long tick); + } + + @FunctionalInterface + public interface ChatCallback { + void onChat(Box3JSEntity entity, String message, long tick); + } + + @FunctionalInterface + public interface FluidEnterCallback { + void onEnter(Box3JSEntity entity, String fluid, int x, int y, int z, long tick); + } + + @FunctionalInterface + public interface FluidLeaveCallback { + void onLeave(Box3JSEntity entity, String fluid, int x, int y, int z, long tick); + } + + @FunctionalInterface + public interface EntityContactCallback { + void onContact(Box3JSEntity entity, Box3JSEntity other, long tick); + } + + @FunctionalInterface + public interface EntitySeparateCallback { + void onSeparate(Box3JSEntity entity, Box3JSEntity other, long tick); + } + + @FunctionalInterface + public interface BlockPlaceCallback { + void onPlace(Box3JSEntity entity, int x, int y, int z, String voxel, int voxelId, long tick); + } + + @FunctionalInterface + public interface EntityDeathCallback { + void onDeath(Box3JSEntity entity, Box3JSEntity killer, long tick); + } + + @FunctionalInterface + public interface PlayerRespawnCallback { + void onRespawn(Box3JSEntity entity); + } + + @FunctionalInterface + public interface BlockActivateCallback { + void onActivate(Box3JSEntity entity, int x, int y, int z, String voxel, long tick); + } + + @FunctionalInterface + public interface EntityDamageCallback { + void onDamage(Box3JSEntity entity, double amount, String source, Box3JSEntity attacker, long tick); + } + + // ---- Namespace inner classes ---- + + public class ScoreboardNS { + public void add(String name) { add(name, "dummy"); } + public void add(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); + } + public void setScore(Object entityOrName, String objectiveName, int value) { + Scoreboard sb = server.getScoreboard(); + Objective obj = sb.getObjective(objectiveName); + if (obj == null) return; + String name = resolveScoreName(entityOrName); + if (name == null) return; + sb.getOrCreatePlayerScore(ScoreHolder.forNameOnly(name), obj).set(value); + } + public int getScore(Object entityOrName, String objectiveName) { + Scoreboard sb = server.getScoreboard(); + Objective obj = sb.getObjective(objectiveName); + if (obj == null) return 0; + String name = resolveScoreName(entityOrName); + if (name == null) return 0; + ScoreAccess access = sb.getOrCreatePlayerScore(ScoreHolder.forNameOnly(name), obj); + return access.get(); + } + public void show(String slot, String objectiveName) { + Scoreboard sb = server.getScoreboard(); + DisplaySlot displaySlot = switch (slot.toLowerCase()) { + case "list" -> DisplaySlot.LIST; + case "belowname", "below_name" -> DisplaySlot.BELOW_NAME; + default -> DisplaySlot.SIDEBAR; + }; + Objective obj = sb.getObjective(objectiveName); + sb.setDisplayObjective(displaySlot, obj); + } + public void hide(String slot) { + Scoreboard sb = server.getScoreboard(); + DisplaySlot displaySlot = switch (slot.toLowerCase()) { + case "list" -> DisplaySlot.LIST; + case "belowname", "below_name" -> DisplaySlot.BELOW_NAME; + default -> DisplaySlot.SIDEBAR; + }; + sb.setDisplayObjective(displaySlot, null); + } + public void remove(String name) { + Scoreboard sb = server.getScoreboard(); + Objective obj = sb.getObjective(name); + if (obj != null) sb.removeObjective(obj); + } + public java.util.List> list(String objectiveName) { + java.util.List> result = new ArrayList<>(); + Scoreboard sb = server.getScoreboard(); + Objective obj = sb.getObjective(objectiveName); + if (obj == null) return result; + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + int s = sb.getOrCreatePlayerScore(ScoreHolder.forNameOnly(player.getScoreboardName()), obj).get(); + Map m = new LinkedHashMap<>(); + m.put("name", player.getScoreboardName()); + m.put("value", s); + result.add(m); + } + return result; + } + } + + public class BossBarNS { + public void show(String name, String text, double progress, String colorName) { + ServerBossEvent bar = bossBars.get(name); + if (bar == null) { + BossBarColor color = colorName == null ? BossBarColor.WHITE : 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; + }; + bar = new ServerBossEvent(Component.literal(text), color, BossBarOverlay.PROGRESS); + bossBars.put(name, bar); + } else { + bar.setName(Component.literal(text)); + if (colorName != null) bar.setColor(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; + }); + } + bar.setProgress((float) Math.max(0, Math.min(1, progress))); + for (ServerPlayer sp : server.getPlayerList().getPlayers()) bar.addPlayer(sp); + } + public void remove(String name) { + ServerBossEvent bar = bossBars.remove(name); + if (bar != null) bar.removeAllPlayers(); + } + } + + public class TeamNS { + public void create(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)); + } + } + public void join(Object entityOrName, String teamName) { + Scoreboard sb = server.getScoreboard(); + PlayerTeam team = sb.getPlayerTeam(teamName); + if (team == null) return; + String name = resolveScoreName(entityOrName); + if (name != null) sb.addPlayerToTeam(name, team); + } + public void leave(Object entityOrName) { + Scoreboard sb = server.getScoreboard(); + String name = resolveScoreName(entityOrName); + if (name != null) sb.removePlayerFromTeam(name); + } + public void remove(String name) { + Scoreboard sb = server.getScoreboard(); + PlayerTeam team = sb.getPlayerTeam(name); + if (team != null) sb.removePlayerTeam(team); + } + public String of(Object entityOrName) { + Scoreboard sb = server.getScoreboard(); + String name = resolveScoreName(entityOrName); + if (name == null) return null; + PlayerTeam team = sb.getPlayersTeam(name); + return team != null ? team.getName() : null; + } + } + + public class BorderNS { + public double size() { return server.overworld().getWorldBorder().getSize(); } + public void center(double x, double z) { server.overworld().getWorldBorder().setCenter(x, z); } + public void set(double size) { server.overworld().getWorldBorder().setSize(size); } + public void shrink(double targetSize, double seconds) { + WorldBorder border = server.overworld().getWorldBorder(); + border.lerpSizeBetween(border.getSize(), targetSize, (long)(seconds * 1000)); + } + public void damage(double damage) { server.overworld().getWorldBorder().setDamagePerBlock(damage); } + public void warning(int blocks) { server.overworld().getWorldBorder().setWarningBlocks(blocks); } + } + + public class LightningNS { + public boolean strike(double x, double y, double z) { + ServerLevel level = server.overworld(); + LightningBolt bolt = EntityType.LIGHTNING_BOLT.create(level); + if (bolt == null) return false; + bolt.moveTo(x, y, z); + bolt.setVisualOnly(false); + level.addFreshEntity(bolt); + return true; + } + public boolean strike(double x, double y, double z, double damage) { + ServerLevel level = server.overworld(); + LightningBolt bolt = EntityType.LIGHTNING_BOLT.create(level); + if (bolt == null) return false; + bolt.moveTo(x, y, z); + bolt.setDamage((float) damage); + bolt.setVisualOnly(false); + level.addFreshEntity(bolt); + return true; + } + } + + public class FireworkNS { + public void launch(double x, double y, double z, String color, String shape) { + ServerLevel level = server.overworld(); + int colorInt = switch (color != null ? color.toLowerCase(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(level, x, y, z, rocket); + level.addFreshEntity(entity); + } + } + + public class ParticleNS { + public void spawn(String type, double x, double y, double z, int count, double dx, double dy, double dz, double speed) { + var particle = resolveParticle(type); + if (particle != null) { + server.overworld().sendParticles(particle, x, y, z, count, dx, dy, dz, speed); + } + } + public void circle(double x, double y, double z, double radius, String type, int count) { + var particle = resolveParticle(type); + if (particle == null) return; + ServerLevel level = server.overworld(); + for (int i = 0; i < count; i++) { + double angle = (2.0 * Math.PI * i) / count; + double px = x + Math.cos(angle) * radius; + double pz = z + Math.sin(angle) * radius; + level.sendParticles(particle, px, y, pz, 1, 0, 0, 0, 0); + } + } + private ParticleOptions resolveParticle(String type) { + ResourceLocation rl = ResourceLocation.tryParse(type); + if (rl == null) return null; + var particle = BuiltInRegistries.PARTICLE_TYPE.getOptional(rl); + if (particle.isEmpty()) return null; + var p = particle.get(); + if (p instanceof ParticleOptions options) return options; + return null; + } + } + + public class DropNS { + public void item(double x, double y, double z, String itemId, int count) { + ServerLevel level = server.overworld(); + ResourceLocation rl = ResourceLocation.tryParse(itemId); + if (rl == null) return; + var item = BuiltInRegistries.ITEM.getOptional(rl).orElse(null); + if (item == null) return; + ItemStack stack = new ItemStack(item, Math.max(1, count)); + ItemEntity itemEntity = new ItemEntity(level, x, y, z, stack); + level.addFreshEntity(itemEntity); + } + } +} 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..e4ecd328 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptCommand.java @@ -0,0 +1,169 @@ +package com.box3lab.box3js.script; + +import com.mojang.brigadier.arguments.StringArgumentType; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.neoforged.neoforge.event.RegisterCommandsEvent; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static net.minecraft.commands.Commands.literal; +import static net.minecraft.commands.Commands.argument; + +public class Box3ScriptCommand { + + private static Path scriptDir; + + public static void register(RegisterCommandsEvent event) { + var dispatcher = event.getDispatcher(); + + dispatcher.register( + literal("box3script") + .requires(src -> src.hasPermission(2)) + // --- eval --- + .then(literal("eval") + .then(argument("code", StringArgumentType.greedyString()) + .executes(ctx -> { + String code = StringArgumentType.getString(ctx, "code"); + var server = ctx.getSource().getServer(); + try { + Box3ScriptEngine.get().init(server); + Box3ScriptEngine.get().eval(code); + ctx.getSource().sendSuccess( + () -> Component.literal("Script executed."), false); + } catch (Exception e) { + ctx.getSource().sendFailure( + Component.literal("Script error: " + e.getMessage())); + e.printStackTrace(); + } + return 1; + }))) + // --- file --- + .then(literal("file") + .then(argument("path", StringArgumentType.greedyString()) + .executes(ctx -> { + String input = StringArgumentType.getString(ctx, "path"); + CommandSourceStack src = ctx.getSource(); + var server = src.getServer(); + Path filePath = resolve(input); + if (!Files.exists(filePath)) { + src.sendFailure(Component.literal("File not found: " + filePath)); + return 0; + } + try { + Box3ScriptEngine.get().init(server); + Box3ScriptEngine.get().eval(Files.readString(filePath)); + src.sendSuccess( + () -> Component.literal("Executed: " + filePath.getFileName()), false); + } catch (IOException e) { + src.sendFailure(Component.literal("Failed to read file: " + e.getMessage())); + } catch (Exception e) { + src.sendFailure(Component.literal("Script error: " + e.getMessage())); + e.printStackTrace(); + } + return 1; + }))) + // --- stop --- + .then(literal("stop") + .executes(ctx -> { + Box3ScriptEngine.get().reset(); + ctx.getSource().sendSuccess( + () -> Component.literal("All scripts stopped. Callbacks cleared, scope reset."), + false); + return 1; + })) + // --- list --- + .then(literal("list") + .executes(ctx -> { + var server = ctx.getSource().getServer(); + var config = Box3ScriptConfig.get(); + config.discover(server); + var projects = config.listProjects(); + if (projects.isEmpty()) { + ctx.getSource().sendSuccess( + () -> Component.literal("No projects found in config/box3/script/"), + false); + } else { + StringBuilder sb = new StringBuilder("Projects:\n"); + projects.forEach((name, enabled) -> { + sb.append(" ").append(enabled ? "§a[ON]" : "§c[OFF]") + .append(" ").append(name).append("\n"); + }); + ctx.getSource().sendSuccess( + () -> Component.literal(sb.toString().trim()), + false); + } + return 1; + })) + // --- on --- + .then(literal("on") + .then(argument("project", StringArgumentType.word()) + .executes(ctx -> { + String project = StringArgumentType.getString(ctx, "project"); + Box3ScriptConfig.get().setEnabled(project, true); + ctx.getSource().sendSuccess( + () -> Component.literal("Enabled: " + project), + false); + return 1; + }))) + // --- off --- + .then(literal("off") + .then(argument("project", StringArgumentType.word()) + .executes(ctx -> { + String project = StringArgumentType.getString(ctx, "project"); + Box3ScriptConfig.get().setEnabled(project, false); + ctx.getSource().sendSuccess( + () -> Component.literal("Disabled: " + project), + false); + return 1; + }))) + // --- reload --- + .then(literal("reload") + .executes(ctx -> { + var server = ctx.getSource().getServer(); + Box3ScriptEngine.get().reset(); + Box3ScriptEngine.get().autoLoad(server); + ctx.getSource().sendSuccess( + () -> Component.literal("Scripts reloaded."), + false); + return 1; + })) + // --- run --- + .then(literal("run") + .then(argument("project", StringArgumentType.greedyString()) + .executes(ctx -> { + String project = StringArgumentType.getString(ctx, "project"); + CommandSourceStack src = ctx.getSource(); + var server = src.getServer(); + Path appJs = resolve(project).resolve("app.js"); + if (!Files.exists(appJs)) { + src.sendFailure(Component.literal("app.js not found: " + appJs)); + return 0; + } + try { + Box3ScriptEngine.get().init(server); + Box3ScriptEngine.get().eval(Files.readString(appJs)); + src.sendSuccess( + () -> Component.literal("Executed: " + project + "/app.js"), false); + } catch (IOException e) { + src.sendFailure(Component.literal("Failed to read: " + e.getMessage())); + } catch (Exception e) { + src.sendFailure(Component.literal("Script error: " + e.getMessage())); + e.printStackTrace(); + } + return 1; + }))) + ); + } + + private static Path resolve(String input) { + Path p = Path.of(input); + if (p.isAbsolute()) return p; + if (scriptDir == null) { + scriptDir = Path.of("config", "box3", "script").toAbsolutePath(); + } + return scriptDir.resolve(input).normalize(); + } +} 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..7e7a1bfe --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptConfig.java @@ -0,0 +1,84 @@ +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 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..0688d864 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java @@ -0,0 +1,502 @@ +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 java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +public class Box3ScriptEngine { + + private static final Box3ScriptEngine INSTANCE = new Box3ScriptEngine(); + + private ScriptableObject scope; + private Box3JSWorld worldBinding; + private Box3JSVoxels voxelsBinding; + private Box3JSStorage storageBinding; + private MinecraftServer server; + private boolean initialized; + + private final List tickCallbacks = new CopyOnWriteArrayList<>(); + private final List joinCallbacks = new CopyOnWriteArrayList<>(); + private final List leaveCallbacks = new CopyOnWriteArrayList<>(); + private final List voxelDestroyCallbacks = new CopyOnWriteArrayList<>(); + private final List voxelContactCallbacks = new CopyOnWriteArrayList<>(); + private final List interactCallbacks = new CopyOnWriteArrayList<>(); + private final List chatCallbacks = new CopyOnWriteArrayList<>(); + private final List fluidEnterCallbacks = new CopyOnWriteArrayList<>(); + private final List fluidLeaveCallbacks = new CopyOnWriteArrayList<>(); + private final List entityContactCallbacks = new CopyOnWriteArrayList<>(); + private final List entitySeparateCallbacks = new CopyOnWriteArrayList<>(); + private final List blockPlaceCallbacks = new CopyOnWriteArrayList<>(); + private final List entityDeathCallbacks = new CopyOnWriteArrayList<>(); + private final List respawnCallbacks = new CopyOnWriteArrayList<>(); + private final List blockActivateCallbacks = new CopyOnWriteArrayList<>(); + private final List entityDamageCallbacks = new CopyOnWriteArrayList<>(); + private final Map voxelContactTracked = new ConcurrentHashMap<>(); + private final Map fluidStateTracked = new ConcurrentHashMap<>(); + private final Set entityContactPairs = ConcurrentHashMap.newKeySet(); + private final Map playerChatHandlers = new ConcurrentHashMap<>(); + private final Map> entityCustomProps = new HashMap<>(); + private final List timers = new ArrayList<>(); + private int timerIdCounter; + + public static Box3ScriptEngine get() { + return INSTANCE; + } + + public void init(MinecraftServer server) { + if (initialized) return; + this.server = server; + this.worldBinding = new Box3JSWorld(server, this); + this.voxelsBinding = new Box3JSVoxels(server); + this.storageBinding = new Box3JSStorage(server.getServerDirectory().resolve("config"), this); + + Context cx = Context.enter(); + try { + scope = cx.initStandardObjects(); + + // World and console bindings + 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, "console", Context.javaToJS(new Box3JSConsole(), scope)); + + // sleep(ms) function + 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; + } + }); + + // Math types — NativeJavaClass wraps so `new GameVector3(x,y,z)` works + 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)); + + // Enums + 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(); + } + + 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("app.js"); + if (Files.exists(appJs) && config.isEnabled(name)) { + try { + eval(Files.readString(appJs)); + Box3JS.LOGGER.info("Auto-loaded project: {}", name); + } catch (Exception e) { + Box3JS.LOGGER.error("Failed to auto-load: {}", appJs, e); + } + } + }); + } 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(); + } + } + + /** Tick callback from Box3JSWorld */ + public void addTickCallback(Runnable cb) { tickCallbacks.add(cb); } + public void addJoinCallback(Box3JSWorld.PlayerJoinCallback cb) { joinCallbacks.add(cb); } + public void addLeaveCallback(Box3JSWorld.PlayerLeaveCallback cb) { leaveCallbacks.add(cb); } + public void addVoxelDestroyCallback(Box3JSWorld.VoxelDestroyCallback cb) { voxelDestroyCallbacks.add(cb); } + public void addVoxelContactCallback(Box3JSWorld.VoxelContactCallback cb) { voxelContactCallbacks.add(cb); } + public void addInteractCallback(Box3JSWorld.InteractCallback cb) { interactCallbacks.add(cb); } + public void addChatCallback(Box3JSWorld.ChatCallback cb) { chatCallbacks.add(cb); } + public void addFluidEnterCallback(Box3JSWorld.FluidEnterCallback cb) { fluidEnterCallbacks.add(cb); } + public void addFluidLeaveCallback(Box3JSWorld.FluidLeaveCallback cb) { fluidLeaveCallbacks.add(cb); } + public void addEntityContactCallback(Box3JSWorld.EntityContactCallback cb) { entityContactCallbacks.add(cb); } + public void addEntitySeparateCallback(Box3JSWorld.EntitySeparateCallback cb) { entitySeparateCallbacks.add(cb); } + public void addBlockPlaceCallback(Box3JSWorld.BlockPlaceCallback cb) { blockPlaceCallbacks.add(cb); } + public void addEntityDeathCallback(Box3JSWorld.EntityDeathCallback cb) { entityDeathCallbacks.add(cb); } + public void addRespawnCallback(Box3JSWorld.PlayerRespawnCallback cb) { respawnCallbacks.add(cb); } + public void addBlockActivateCallback(Box3JSWorld.BlockActivateCallback cb) { blockActivateCallbacks.add(cb); } + public void addEntityDamageCallback(Box3JSWorld.EntityDamageCallback cb) { entityDamageCallbacks.add(cb); } + public void setPlayerChatHandler(UUID uuid, Function handler) { playerChatHandlers.put(uuid, handler); } + + public int scheduleTimeout(Function handler, int ticks) { + int id = ++timerIdCounter; + timers.add(new TimerEntry(id, handler, ticks, 0)); + return id; + } + + public int scheduleInterval(Function handler, int ticks) { + int id = ++timerIdCounter; + timers.add(new TimerEntry(id, handler, ticks, ticks)); + return id; + } + + public void clearTimer(int id) { + timers.removeIf(t -> t.id == id); + } + + private void fireTimers() { + var toFire = new ArrayList(); + var toRemove = new ArrayList(); + for (var t : timers) { + if (--t.remaining <= 0) { + toFire.add(t); + if (t.interval == 0) { + toRemove.add(t); + } else { + t.remaining = t.interval; + } + } + } + timers.removeAll(toRemove); + for (var t : toFire) { + callFunction(t.handler); + } + } + + public void fireTick() { + fireTimers(); + for (Runnable cb : tickCallbacks) cb.run(); + // Voxel contact tracking: check if any tracked entity changed block position + if (!voxelContactCallbacks.isEmpty()) { + long tick = server.getTickCount(); + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + UUID uuid = player.getUUID(); + BlockPos current = player.blockPosition(); + BlockPos last = voxelContactTracked.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(); + for (var cb : voxelContactCallbacks) { + cb.onContact(entity, voxelId, current.getX(), current.getY(), current.getZ(), 1, force, tick); + } + } + } + } + // Fluid state tracking + if (!fluidEnterCallbacks.isEmpty() || !fluidLeaveCallbacks.isEmpty()) { + long tick = server.getTickCount(); + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + UUID uuid = player.getUUID(); + String current = player.isInLava() ? "lava" : player.isInWater() ? "water" : "none"; + String last = fluidStateTracked.put(uuid, current); + if (!current.equals(last)) { + Box3JSEntity entity = new Box3JSEntity(player, server, this); + BlockPos pos = player.blockPosition(); + if (!"none".equals(current) && !"none".equals(last) && last != null) { + // Switched fluid type (water→lava or lava→water) + for (var cb : fluidLeaveCallbacks) { + cb.onLeave(entity, last, pos.getX(), pos.getY(), pos.getZ(), tick); + } + for (var cb : fluidEnterCallbacks) { + cb.onEnter(entity, current, pos.getX(), pos.getY(), pos.getZ(), tick); + } + } else if (!"none".equals(current) && ("none".equals(last) || last == null)) { + for (var cb : fluidEnterCallbacks) { + cb.onEnter(entity, current, pos.getX(), pos.getY(), pos.getZ(), tick); + } + } else if ("none".equals(current) && last != null && !"none".equals(last)) { + for (var cb : fluidLeaveCallbacks) { + cb.onLeave(entity, last, pos.getX(), pos.getY(), pos.getZ(), tick); + } + } + } + } + } + // Entity contact tracking + if (!entityContactCallbacks.isEmpty()) { + long tick = server.getTickCount(); + 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) { // 1.5 blocks squared + if (entityContactPairs.add(pairKey)) { + Box3JSEntity ea = new Box3JSEntity(a, server, this); + Box3JSEntity eb = new Box3JSEntity(b, server, this); + for (var cb : entityContactCallbacks) { + cb.onContact(ea, eb, tick); + } + } + } else if (entityContactPairs.remove(pairKey)) { + if (!entitySeparateCallbacks.isEmpty()) { + Box3JSEntity ea = new Box3JSEntity(a, server, this); + Box3JSEntity eb = new Box3JSEntity(b, server, this); + for (var cb : entitySeparateCallbacks) { + cb.onSeparate(ea, eb, tick); + } + } + } + } + } + } + } + + 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) { + if (voxelDestroyCallbacks.isEmpty()) return; + Box3JSEntity entity = new Box3JSEntity(player, server, this); + long tick = server.getTickCount(); + String voxel = getBlockIdString(pos); + for (var cb : voxelDestroyCallbacks) { + cb.onDestroy(entity, pos.getX(), pos.getY(), pos.getZ(), voxel, tick); + } + } + + public void fireInteract(ServerPlayer player, net.minecraft.world.entity.Entity target) { + if (interactCallbacks.isEmpty()) return; + Box3JSEntity entity = new Box3JSEntity(player, server, this); + Box3JSEntity targetEntity = new Box3JSEntity(target, server, this); + long tick = server.getTickCount(); + for (var cb : interactCallbacks) { + cb.onInteract(entity, targetEntity, tick); + } + } + + public void fireChat(ServerPlayer player, String message) { + Box3JSEntity entity = new Box3JSEntity(player, server, this); + long tick = server.getTickCount(); + // Global chat callbacks + for (var cb : chatCallbacks) { + cb.onChat(entity, message, tick); + } + // Per-player chat handler + Function playerHandler = playerChatHandlers.get(player.getUUID()); + if (playerHandler != null) { + callFunction(playerHandler, entity, message, tick); + } + } + + public void fireBlockPlace(ServerPlayer player, BlockPos pos, BlockState state) { + if (blockPlaceCallbacks.isEmpty()) return; + Box3JSEntity entity = new Box3JSEntity(player, server, this); + long tick = server.getTickCount(); + int voxelId = voxelsBinding.getId(state); + String voxel = state.isAir() ? "minecraft:air" : state.getBlock().builtInRegistryHolder().key().location().toString(); + for (var cb : blockPlaceCallbacks) { + cb.onPlace(entity, pos.getX(), pos.getY(), pos.getZ(), voxel, voxelId, tick); + } + } + + public void fireEntityDeath(net.minecraft.world.entity.Entity deadEntity, net.minecraft.world.entity.Entity attacker) { + if (entityDeathCallbacks.isEmpty()) return; + Box3JSEntity entity = new Box3JSEntity(deadEntity, server, this); + Box3JSEntity killer = attacker != null ? new Box3JSEntity(attacker, server, this) : null; + long tick = server.getTickCount(); + for (var cb : entityDeathCallbacks) { + cb.onDeath(entity, killer, tick); + } + } + + public void firePlayerRespawn(ServerPlayer player) { + if (respawnCallbacks.isEmpty()) return; + Box3JSEntity entity = new Box3JSEntity(player, server, this); + for (var cb : respawnCallbacks) { + cb.onRespawn(entity); + } + } + + public void fireBlockActivate(ServerPlayer player, BlockPos pos, BlockState state) { + if (blockActivateCallbacks.isEmpty()) return; + Box3JSEntity entity = new Box3JSEntity(player, server, this); + long tick = server.getTickCount(); + String voxel = state.isAir() ? "minecraft:air" : state.getBlock().builtInRegistryHolder().key().location().toString(); + for (var cb : blockActivateCallbacks) { + cb.onActivate(entity, pos.getX(), pos.getY(), pos.getZ(), voxel, tick); + } + } + + public void fireEntityDamage(net.minecraft.world.entity.Entity damagedEntity, double amount, String source, net.minecraft.world.entity.Entity attacker) { + if (entityDamageCallbacks.isEmpty()) return; + Box3JSEntity entity = new Box3JSEntity(damagedEntity, server, this); + Box3JSEntity attackerEntity = attacker != null ? new Box3JSEntity(attacker, server, this) : null; + long tick = server.getTickCount(); + for (var cb : entityDamageCallbacks) { + cb.onDamage(entity, amount, source, attackerEntity, tick); + } + } + + public void firePlayerJoin(ServerPlayer player) { + Box3JSEntity entity = new Box3JSEntity(player, server, this); + for (var cb : joinCallbacks) cb.onJoin(entity); + } + + public void firePlayerLeave(ServerPlayer player) { + Box3JSEntity entity = new Box3JSEntity(player, server, this); + for (var cb : leaveCallbacks) cb.onLeave(entity); + } + + /** 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 entityCustomProps.computeIfAbsent(uuid, k -> new HashMap<>()); + } + + public void clearCustomProps(UUID uuid) { + entityCustomProps.remove(uuid); + } + + /** Clear all callbacks and reset the JS scope (keeps server binding) */ + public void reset() { + 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(); + voxelContactTracked.clear(); + fluidStateTracked.clear(); + entityContactPairs.clear(); + playerChatHandlers.clear(); + entityCustomProps.clear(); + timers.clear(); + timerIdCounter = 0; + Box3JSWorld freshWorld = new Box3JSWorld(server, this); + this.worldBinding = freshWorld; + Box3JSVoxels freshVoxels = new Box3JSVoxels(server); + this.voxelsBinding = freshVoxels; + Box3JSStorage freshStorage = new Box3JSStorage(server.getServerDirectory().resolve("config"), this); + this.storageBinding = freshStorage; + Context cx = Context.enter(); + try { + scope = cx.initStandardObjects(); + ScriptableObject.putProperty(scope, "world", Context.javaToJS(freshWorld, scope)); + ScriptableObject.putProperty(scope, "voxels", Context.javaToJS(freshVoxels, scope)); + ScriptableObject.putProperty(scope, "storage", Context.javaToJS(freshStorage, scope)); + ScriptableObject.putProperty(scope, "console", Context.javaToJS(new Box3JSConsole(), scope)); + 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 static class Box3JSConsole { + public void log(Object... args) { + StringBuilder sb = new StringBuilder(); + for (Object a : args) sb.append(a).append(' '); + System.out.println("[Box3JS] " + sb.toString().trim()); + } + public void clear() { + System.out.print("\033[H\033[2J"); + System.out.flush(); + } + } + + private static class TimerEntry { + final int id; + final Function handler; + int remaining; + final int interval; + + TimerEntry(int id, Function handler, int remaining, int interval) { + this.id = id; + this.handler = handler; + this.remaining = remaining; + this.interval = interval; + } + } +} 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/lang/en_us.json b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/en_us.json new file mode 100644 index 00000000..0cb448fd --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/en_us.json @@ -0,0 +1,13 @@ +{ + "itemGroup.box3js": "Example Mod Tab", + "block.box3js.example_block": "Example Block", + "item.box3js.example_item": "Example Item", + + "box3js.configuration.title": "Box3JS Configs", + "box3js.configuration.section.box3js.common.toml": "Box3JS Configs", + "box3js.configuration.section.box3js.common.toml.title": "Box3JS Configs", + "box3js.configuration.items": "Item List", + "box3js.configuration.logDirtBlock": "Log Dirt Block", + "box3js.configuration.magicNumberIntroduction": "Magic Number Text", + "box3js.configuration.magicNumber": "Magic Number" +} 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..b3e1b07d --- /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 模组为服务端引入 JavaScript 运行时,支持约 100 项 Box3 风格 API 与 Minecraft 原生扩展的补充,无需 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,)" From 11725deb08d945d25fc7dccec350a535c8a376c2 Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Thu, 30 Apr 2026 12:21:32 +0800 Subject: [PATCH 02/17] =?UTF-8?q?docs(box3-api):=20=E6=9B=B4=E6=96=B0=20AP?= =?UTF-8?q?I=20=E6=96=87=E6=A1=A3=E5=B9=B6=E9=87=8D=E6=9E=84=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E7=A9=BA=E9=97=B4=EF=BC=9Brefactor(box3js-entity,box3?= =?UTF-8?q?js-player,config):=20=E4=BD=BF=E7=94=A8=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E7=A9=BA=E9=97=B4=E6=A8=A1=E5=BC=8F=E9=87=8D=E6=9E=84=E5=AE=9E?= =?UTF-8?q?=E4=BD=93=E5=92=8C=E7=8E=A9=E5=AE=B6=20API=20=E5=B9=B6=E6=B8=85?= =?UTF-8?q?=E7=90=86=E9=85=8D=E7=BD=AE=EF=BC=9Bfeat(box3js-storage):=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20storage.keys()=20=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除未使用的"🚫 不适用"状态分类 - 新增 console API 完整文档(log/debug/warn/error/assert/clear) - 新增 sleep API 线程阻塞功能说明 - 新增 world 命名空间下的项目间消息系统文档 - 重构 world API 为 query/effect/sound 子命名空间 - 更新 entity API 使用 equipment/effect 命名空间模式 - 重构 player API 为 inventory/effect/sound 命名空间 - 新增 store.keys() 方法文档 - 新增 /box3script create 命令文档 - 更新命名空间 API 章节以明确 MC 扩展边界 - 更新统计数据和最后修订时间 - 新增 EffectNS 和 EquipmentNS 嵌套类以优化实体 API 代码组织 - 将 addEffect() 和 setEquipment() 方法移至对应命名空间 - 移除实体类中冗余的 sound 方法 - 创建 InventoryNS、EffectNS 和 SoundNS 嵌套类用于玩家 API - 将 inventory、effect 和 sound 相关方法移至其命名空间 - 用 NativeObject 替代 LinkedHashMap 以保持一致性 - 使用 ScriptableObject 实现正确的 JavaScript 对象处理 - 实现 keys() 方法,以字符串数组形式返回所有存储键 - 无需遍历即可访问 storage 中所有可用键 - 保持与现有存储操作的一致性 - 移除 Config 类中未使用的导入和示例配置选项 - 移除演示配置和校验逻辑以简化 Config 类 - 仅保留核心配置规范构建器 --- .../docs/BOX3_API_MAPPING.md | 68 ++-- .../main/java/com/box3lab/box3js/Config.java | 32 -- .../box3lab/box3js/script/Box3JSEntity.java | 87 ++--- .../box3lab/box3js/script/Box3JSPlayer.java | 144 ++++---- .../box3lab/box3js/script/Box3JSStorage.java | 6 +- .../box3lab/box3js/script/Box3JSWorld.java | 346 +++++++++++------- .../box3js/script/Box3ScriptCommand.java | 43 ++- .../box3js/script/Box3ScriptEngine.java | 164 +++++---- .../templates/META-INF/neoforge.mods.toml | 2 +- 9 files changed, 517 insertions(+), 375 deletions(-) diff --git a/Box3JS-NeoForge-1.21.1/docs/BOX3_API_MAPPING.md b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_MAPPING.md index 55f7a5ef..2367ad42 100644 --- a/Box3JS-NeoForge-1.21.1/docs/BOX3_API_MAPPING.md +++ b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_MAPPING.md @@ -6,7 +6,6 @@ - **✅ Box3 API** — Box3 原有 API,已接入 MC - **⬆ MC 扩展** — 非 Box3 原有,利用 MC 特性新增 -- **🚫 不适用** — 无 MC 对应概念 ### 运行时 @@ -16,6 +15,23 @@ | 作用域 | 服务端脚本(S- 脚本) | | Tick | `ServerTickEvent.Post` | +### console (⬆ MC 扩展) + +| API | 说明 | +|---|---| +| `console.log(...args)` | 标准日志输出 `[Box3JS] [项目名] msg` | +| `console.debug(...args)` | 调试日志 `[Box3JS][DEBUG] [项目名] msg` | +| `console.warn(...args)` | 警告日志 `[Box3JS][WARN] [项目名] msg` | +| `console.error(...args)` | 错误日志(stderr)`[Box3JS][ERROR] [项目名] msg` | +| `console.assert(assertion, ...args)` | 断言失败时调用 `error()` | +| `console.clear()` | 清空控制台 | + +### sleep (⬆ MC 扩展) + +| API | 说明 | +|---|---| +| `sleep(ms)` | 阻塞当前线程指定毫秒数 | + --- ## world (GameWorld) @@ -90,18 +106,25 @@ | `world.clearTimeout(id)` | ⬆ | 取消 timeout | | `world.clearInterval(id)` | ⬆ | 取消 interval | +### 项目间消息传递(⬆ MC 扩展) + +| API | 类型 | 说明 | +|---|---|---| +| `world.message.send(target, data)` | ⬆ | 精确路由到目标项目;`"*"` 广播给所有其他项目 | +| `world.message.on(handler)` | ⬆ | 接收其他项目发来的消息;`handler(from, data)` | + ### 命令 / 查询(⬆ MC 扩展) | API | 类型 | 说明 | |---|---|---| | `world.runCommand(cmd)` | ⬆ | 以服务器身份执行命令 | -| `world.getEntitiesInArea(pos1, pos2)` | ⬆ | 返回 AABB 区域内所有实体 | -| `world.raycast(origin, dir)` | ⬆ | 射线检测,默认 5 格 | -| `world.raycast(origin, dir, dist)` | ⬆ | 射线检测,自定义距离;返回 `{hit, x, y, z, voxel, entity, normalX/Y/Z, distance}` | -| `world.explode(x, y, z, power)` | ⬆ | 创建爆炸 | -| `world.explode(x, y, z, power, fire)` | ⬆ | 创建爆炸(可引火) | -| `world.playSoundAll(path, x, y, z, vol, pitch)` | ⬆ | 在坐标播放音效给所有玩家 | -| `world.getBiome(x, y, z)` | ⬆ | 获取生物群系命名空间 ID | +| `world.query.raycast(origin, dir)` | ⬆ | 射线检测,默认 5 格 | +| `world.query.raycast(origin, dir, dist)` | ⬆ | 射线检测,自定义距离 | +| `world.query.entitiesInArea(pos1, pos2)` | ⬆ | 返回 AABB 区域内所有实体 | +| `world.query.biome(x, y, z)` | ⬆ | 获取生物群系命名空间 ID | +| `world.effect.explode(x, y, z, power)` | ⬆ | 创建爆炸 | +| `world.effect.explode(x, y, z, power, fire)` | ⬆ | 创建爆炸(可引火) | +| `world.sound.playAll(path, x, y, z, vol, pitch)` | ⬆ | 在坐标播放音效给所有玩家 | --- ## entity (GameEntity) @@ -132,8 +155,8 @@ | `.lookAt(x, y, z)` | ⬆ | 实体面朝指定坐标 | | `.navigateTo(x, y, z, speed)` | ⬆ | 寻路步行到目标(PathfinderMob) | | `.setAI(enabled)` | ⬆ | 开关实体 AI | -| `.setEquipment(slot, itemId)` | ⬆ | 给生物穿装备;slot: mainhand/offhand/head/chest/legs/feet | -| `.addEffect(effectId, dur, amp)` | ⬆ | 给任意 LivingEntity 添加药水效果 | +| `.equipment.set(slot, itemId)` | ⬆ | 给生物穿装备;slot: mainhand/offhand/head/chest/legs/feet | +| `.effect.add(effectId, dur, amp)` | ⬆ | 给任意 LivingEntity 添加药水效果 | | `.setTarget(entity)` | ⬆ | 设置怪物攻击目标(Mob.setTarget) | | `.getTarget()` | ⬆ | 获取怪物当前攻击目标 | | `.clearTarget()` | ⬆ | 清除攻击目标 | @@ -219,9 +242,9 @@ | API | 类型 | 说明 | |---|---|---| -| `.giveItem(itemId, count)` | ⬆ | 命名空间 ID | -| `.addEffect(effectId, dur, amp)` | ⬆ | 命名空间 ID;duration 为 tick | -| `.clearEffects()` | ⬆ | `removeAllEffects()` | +| `.inventory.give(itemId, count)` | ⬆ | 命名空间 ID | +| `.effect.add(effectId, dur, amp)` | ⬆ | 命名空间 ID;duration 为 tick | +| `.effect.clear()` | ⬆ | `removeAllEffects()` | | `.xp` | ⬆ | 经验等级 get/set | | `.food` | ⬆ | 饱食度 get/set | | `.saturation` | ⬆ | 饱和度 get/set | @@ -231,15 +254,15 @@ | API | 类型 | 说明 | |---|---|---| | `.sound(path)` | ✅ | 固定 NOTE_BLOCK_PLING | -| `.playSound(path, vol, pitch)` | ⬆ | 播放任意 MC 音效 | +| `.sound.play(path, vol, pitch)` | ⬆ | 播放任意 MC 音效 | ### 维度 / 物品(⬆ MC 扩展) | API | 类型 | 说明 | |---|---|---| | `.dimension` | ⬆ | 维度 ID,get/set(set 可跨维度传送) | -| `.getHeldItem()` | ⬆ | 主手物品 `{id, count}` | -| `.clearInventory()` | ⬆ | 清空背包 | +| `.inventory.held()` | ⬆ | 主手物品 `{id, count}` | +| `.inventory.clear()` | ⬆ | 清空背包 | ### 命令(⬆ MC 扩展) @@ -289,6 +312,7 @@ | `storage.key` | 空字符串 | | `storage.getDataStorage(name)` / `getGroupStorage(name)` | 返回 GameDataStorage | | `store.set(key, value)` / `store.get(key)` | 读写 JSON | +| `store.keys()` | 返回所有 key | | `store.update(key, handler)` | 回调更新 | | `store.remove(key)` / `store.increment(key, delta?)` | 删除/递加 | | `store.list(options)` | 分页排序过滤 | @@ -307,7 +331,8 @@ | `/box3script on ` | 启用项目 | | `/box3script off ` | 禁用项目 | | `/box3script reload` | 重载所有启用项目 | -| `/box3script stop` | 停止所有脚本,清空回调 +| `/box3script stop` | 停止所有脚本,清空回调 | +| `/box3script create ` | 创建新项目目录及 `app.js` 模板 | --- @@ -325,9 +350,9 @@ --- -## 命名空间 API (v2) +## 命名空间 API (v2) — 全部为 ⬆ MC 扩展 -`world.*` 下部分功能按分组组织为命名空间调用方式。 +以下 `world.*` / `player.*` / `entity.*` 命名空间 API 均为 MC 原生扩展,非 Box3 原有。 ### world.scoreboard @@ -403,7 +428,6 @@ | 状态 | 数量 | |---|---| | ✅ Box3 API | ~100 | -| ⬆ MC 扩展 | ~72 | -| 🚫 不适用 | ~80 | +| ⬆ MC 扩展 | ~83 | -> 最后更新:2026-04-29 +> 最后更新:2026-04-30 diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Config.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Config.java index 69a2bb81..ab99b7f7 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Config.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Config.java @@ -1,42 +1,10 @@ package com.box3lab.box3js; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.Item; -import net.neoforged.bus.api.SubscribeEvent; -import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.fml.event.config.ModConfigEvent; import net.neoforged.neoforge.common.ModConfigSpec; -// An example config class. This is not required, but it's a good idea to have one to keep your config organized. -// Demonstrates how to use Neo's config APIs public class Config { private static final ModConfigSpec.Builder BUILDER = new ModConfigSpec.Builder(); - public static final ModConfigSpec.BooleanValue LOG_DIRT_BLOCK = BUILDER - .comment("Whether to log the dirt block on common setup") - .define("logDirtBlock", true); - - public static final ModConfigSpec.IntValue MAGIC_NUMBER = BUILDER - .comment("A magic number") - .defineInRange("magicNumber", 42, 0, Integer.MAX_VALUE); - - public static final ModConfigSpec.ConfigValue MAGIC_NUMBER_INTRODUCTION = BUILDER - .comment("What you want the introduction message to be for the magic number") - .define("magicNumberIntroduction", "The magic number is... "); - - // a list of strings that are treated as resource locations for items - public static final ModConfigSpec.ConfigValue> ITEM_STRINGS = BUILDER - .comment("A list of items to log on common setup.") - .defineListAllowEmpty("items", List.of("minecraft:iron_ingot"), () -> "", Config::validateItemName); - static final ModConfigSpec SPEC = BUILDER.build(); - - private static boolean validateItemName(final Object obj) { - return obj instanceof String itemName && BuiltInRegistries.ITEM.containsKey(ResourceLocation.parse(itemName)); - } } 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 index 2eaba2eb..667019aa 100644 --- 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 @@ -30,6 +30,9 @@ public class Box3JSEntity { private Function _onDestroyHandler; private final GameVector3 _position, _velocity, _bounds; + public final EffectNS effect; + public final EquipmentNS equipment; + public Box3JSEntity(Entity entity, MinecraftServer server, Box3ScriptEngine engine) { this.entity = entity; this.server = server; @@ -37,6 +40,8 @@ public Box3JSEntity(Entity entity, MinecraftServer server, Box3ScriptEngine engi 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(); + this.effect = new EffectNS(entity); + this.equipment = new EquipmentNS(entity); } public Entity getEntity() { return entity; } @@ -105,16 +110,6 @@ public void removeTag(String tag) { entity.removeTag(tag); } - // ---- Sound ---- - - public void sound(String path) { - if (entity instanceof ServerPlayer sp) { - sp.playNotifySound( - net.minecraft.sounds.SoundEvents.NOTE_BLOCK_PLING.value(), - SoundSource.PLAYERS, 1.0f, 1.0f); - } - } - // ---- Glowing (MC extension) ---- public boolean isGlowing() { return entity.isCurrentlyGlowing(); } @@ -197,38 +192,6 @@ public void clearFire() { entity.setRemainingFireTicks(0); } - // ---- Equipment & Effects (MC extension) ---- - - /** Set equipment slot for a mob. slot: mainhand/offhand/head/chest/legs/feet */ - public void setEquipment(String slot, String itemId) { - if (!(entity instanceof Mob mob)) return; - EquipmentSlot equipmentSlot = 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; - }; - if (equipmentSlot == null) return; - ResourceLocation rl = ResourceLocation.tryParse(itemId); - if (rl == null) return; - Item item = BuiltInRegistries.ITEM.getOptional(rl).orElse(null); - if (item == null) return; - mob.setItemSlot(equipmentSlot, new ItemStack(item)); - } - - /** Add a potion effect to a LivingEntity. Works on all entities, not just players. */ - public void addEffect(String effectId, int duration, int amplifier) { - if (!(entity instanceof LivingEntity le)) return; - ResourceLocation rl = ResourceLocation.tryParse(effectId); - if (rl == null) return; - Holder effect = BuiltInRegistries.MOB_EFFECT.getHolder(rl).orElse(null); - if (effect == null) return; - le.addEffect(new MobEffectInstance(effect, duration, amplifier)); - } - // ---- Look at (MC extension) ---- public void lookAt(double x, double y, double z) { @@ -331,4 +294,44 @@ public GameVector3 set(double x, double y, double z) { return this; } } + + // ---- Namespace classes ---- + + public static class EffectNS { + private final Entity entity; + EffectNS(Entity entity) { this.entity = entity; } + + public void add(String effectId, int duration, int amplifier) { + if (!(entity instanceof LivingEntity le)) return; + ResourceLocation rl = ResourceLocation.tryParse(effectId); + if (rl == null) return; + Holder effect = BuiltInRegistries.MOB_EFFECT.getHolder(rl).orElse(null); + if (effect == null) return; + le.addEffect(new MobEffectInstance(effect, duration, amplifier)); + } + } + + public static class EquipmentNS { + private final Entity entity; + EquipmentNS(Entity entity) { this.entity = entity; } + + public void set(String slot, String itemId) { + if (!(entity instanceof Mob mob)) return; + EquipmentSlot equipmentSlot = 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; + }; + if (equipmentSlot == null) return; + ResourceLocation rl = ResourceLocation.tryParse(itemId); + if (rl == null) return; + Item item = BuiltInRegistries.ITEM.getOptional(rl).orElse(null); + if (item == null) return; + mob.setItemSlot(equipmentSlot, new ItemStack(item)); + } + } } 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 index a25a7b14..c77391c7 100644 --- 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 @@ -19,8 +19,8 @@ import net.minecraft.world.level.GameType; import org.mozilla.javascript.Function; import org.mozilla.javascript.NativeObject; +import org.mozilla.javascript.ScriptableObject; -import java.util.LinkedHashMap; import java.util.Map; public class Box3JSPlayer { @@ -33,8 +33,15 @@ public Box3JSPlayer(ServerPlayer player, MinecraftServer server, Box3ScriptEngin this.player = player; this.server = server; this.engine = engine; + this.inventory = new InventoryNS(player); + this.effect = new EffectNS(player); + this.sound = new SoundNS(player); } + public final InventoryNS inventory; + public final EffectNS effect; + public final SoundNS sound; + // ---- Info ---- public String getName() { return player.getGameProfile().getName(); } @@ -237,9 +244,9 @@ public Object dialog(NativeObject config) { player.sendSystemMessage(Component.literal(content)); - Map result = new LinkedHashMap<>(); - result.put("index", 0); - result.put("value", opts[0]); + NativeObject result = new NativeObject(); + ScriptableObject.putProperty(result, "index", 0); + ScriptableObject.putProperty(result, "value", opts[0]); return result; } @@ -260,23 +267,6 @@ public void link(String href) { player.sendSystemMessage(comp); } - // ---- Sound ---- - - public void sound(String path) { - player.playNotifySound( - net.minecraft.sounds.SoundEvents.NOTE_BLOCK_PLING.value(), - net.minecraft.sounds.SoundSource.PLAYERS, 1.0f, 1.0f); - } - - public void playSound(String path, float volume, float pitch) { - ResourceLocation rl = ResourceLocation.tryParse(path); - if (rl == null) return; - var sound = net.minecraft.core.registries.BuiltInRegistries.SOUND_EVENT.getOptional(rl); - if (sound.isPresent()) { - player.playNotifySound(sound.get(), net.minecraft.sounds.SoundSource.PLAYERS, volume, pitch); - } - } - // ---- Look at (MC extension) ---- public void lookAt(double x, double y, double z) { @@ -295,51 +285,6 @@ public void runCommand(String cmd) { server.getCommands().performPrefixedCommand(source, cmd); } - // ---- Inventory ---- - - public void giveItem(String itemId, int count) { - ResourceLocation rl = ResourceLocation.tryParse(itemId); - if (rl == null) return; - var item = BuiltInRegistries.ITEM.getOptional(rl); - if (item.isPresent()) { - ItemStack stack = new ItemStack(item.get(), Math.max(1, Math.min(count, 64))); - player.getInventory().add(stack); - } - } - - public void clearInventory() { - player.getInventory().clearContent(); - } - - public Object getHeldItem() { - ItemStack stack = player.getMainHandItem(); - Map result = new LinkedHashMap<>(); - if (stack.isEmpty()) { - result.put("id", "minecraft:air"); - result.put("count", 0); - return result; - } - ResourceLocation key = BuiltInRegistries.ITEM.getKey(stack.getItem()); - result.put("id", key.toString()); - result.put("count", stack.getCount()); - return result; - } - - // ---- Effects ---- - - public void addEffect(String effectId, int duration, int amplifier) { - ResourceLocation rl = ResourceLocation.tryParse(effectId); - if (rl == null) return; - var effect = BuiltInRegistries.MOB_EFFECT.getHolder(rl); - if (effect.isPresent()) { - player.addEffect(new MobEffectInstance(effect.get(), duration, amplifier)); - } - } - - public void clearEffects() { - player.removeAllEffects(); - } - // ---- XP / Food ---- public int getXp() { return player.experienceLevel; } @@ -366,4 +311,71 @@ private T getProp(String key, T defaultValue) { private void setProp(String key, Object value) { props().put(key, value); } + + // ---- Namespace classes ---- + + public static class InventoryNS { + private final ServerPlayer player; + InventoryNS(ServerPlayer player) { this.player = player; } + + public void give(String itemId, int count) { + ResourceLocation rl = ResourceLocation.tryParse(itemId); + if (rl == null) return; + var item = BuiltInRegistries.ITEM.getOptional(rl); + if (item.isPresent()) { + ItemStack stack = new ItemStack(item.get(), Math.max(1, Math.min(count, 64))); + player.getInventory().add(stack); + } + } + + public Object held() { + 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 clear() { + player.getInventory().clearContent(); + } + } + + public static class EffectNS { + private final ServerPlayer player; + EffectNS(ServerPlayer player) { this.player = player; } + + public void add(String effectId, int duration, int amplifier) { + ResourceLocation rl = ResourceLocation.tryParse(effectId); + if (rl == null) return; + var effect = BuiltInRegistries.MOB_EFFECT.getHolder(rl); + if (effect.isPresent()) { + player.addEffect(new MobEffectInstance(effect.get(), duration, amplifier)); + } + } + + public void clear() { + player.removeAllEffects(); + } + } + + public static class SoundNS { + private final ServerPlayer player; + SoundNS(ServerPlayer player) { this.player = player; } + + public void play(String path, double volume, double pitch) { + ResourceLocation rl = ResourceLocation.tryParse(path); + if (rl == null) return; + var sound = net.minecraft.core.registries.BuiltInRegistries.SOUND_EVENT.getOptional(rl); + if (sound.isPresent()) { + player.playNotifySound(sound.get(), net.minecraft.sounds.SoundSource.PLAYERS, (float) volume, (float) pitch); + } + } + } } 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 index 6ea47b70..6451b6b1 100644 --- 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 @@ -174,6 +174,11 @@ public Object get(String key) { return entry != null ? entry.value : null; } + /** keys(): string[] — returns all keys in this storage */ + public String[] keys() { + return read().keySet().toArray(new String[0]); + } + /** update(key: string, handler: function(prevValue): value): void */ public void update(String key, Function handler) { if (key == null || handler == null) return; @@ -311,7 +316,6 @@ private double extractSortValue(Object value, String target) { /** destroy(): void — delete this data storage space */ public void destroy() { - try { Files.deleteIfExists(path); } catch (IOException ignored) {} synchronized (this) { try { Files.deleteIfExists(path); } catch (IOException ignored) {} } 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 index ec982cf4..8adb51c8 100644 --- 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 @@ -1,5 +1,8 @@ package com.box3lab.box3js.script; +import org.mozilla.javascript.NativeObject; +import org.mozilla.javascript.ScriptableObject; + import net.minecraft.commands.CommandSourceStack; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; @@ -53,22 +56,40 @@ public class Box3JSWorld { private final MinecraftServer server; private final Box3ScriptEngine engine; + private String projectName; private final Map bossBars = new HashMap<>(); + public final ScoreboardNS scoreboard; + public final BossBarNS bossbar; + public final TeamNS team; + public final BorderNS border; + public final LightningNS lightning; + public final FireworkNS firework; + public final ParticleNS particle; + public final DropNS drop; + public final QueryNS query; + public final EffectNS effect; + public final SoundNS sound; + public final MessageNS message; + public Box3JSWorld(MinecraftServer server, Box3ScriptEngine engine) { this.server = server; this.engine = engine; - } - - // ---- Namespace fields ---- - public final ScoreboardNS scoreboard = new ScoreboardNS(); - public final BossBarNS bossbar = new BossBarNS(); - public final TeamNS team = new TeamNS(); - public final BorderNS border = new BorderNS(); - public final LightningNS lightning = new LightningNS(); - public final FireworkNS firework = new FireworkNS(); - public final ParticleNS particle = new ParticleNS(); - public final DropNS drop = new DropNS(); + this.scoreboard = new ScoreboardNS(server); + this.bossbar = new BossBarNS(server, bossBars); + this.team = new TeamNS(server); + this.border = new BorderNS(server); + this.lightning = new LightningNS(server); + this.firework = new FireworkNS(server); + this.particle = new ParticleNS(server); + this.drop = new DropNS(server); + this.query = new QueryNS(server, engine); + this.effect = new EffectNS(server); + this.sound = new SoundNS(server); + this.message = new MessageNS(engine); + } + + public void setProjectName(String name) { this.projectName = name; } // ---- World properties ---- @@ -287,127 +308,13 @@ public void runCommand(String cmd) { server.getCommands().performPrefixedCommand(source, cmd); } - private String resolveScoreName(Object entityOrName) { + private 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; } - // ---- Entity query (MC extension) ---- - - public List getEntitiesInArea(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, e -> true)) { - result.add(new Box3JSEntity(e, server, engine)); - } - return result; - } - - // ---- Raycast (MC extension) ---- - - public Object raycast(GameVector3 origin, GameVector3 direction) { - return raycast(origin, direction, 5.0); - } - - public 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) { - Map result = new LinkedHashMap<>(); - result.put("hit", false); - return result; - } - Vec3 dir = new Vec3(direction.x / len, direction.y / len, direction.z / len); - Vec3 end = start.add(dir.scale(maxDistance)); - - // Block raycast - ClipContext ctx = new ClipContext(start, end, ClipContext.Block.OUTLINE, ClipContext.Fluid.NONE, CollisionContext.empty()); - BlockHitResult blockHit = level.clip(ctx); - - // Entity raycast - 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, e -> 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; - - Map result = new LinkedHashMap<>(); - if (closestEntity != null && closestEntDistSqr < blockDistSqr) { - result.put("hit", true); - result.put("x", entityHitPos.x); - result.put("y", entityHitPos.y); - result.put("z", entityHitPos.z); - result.put("normalX", 0); - result.put("normalY", 0); - result.put("normalZ", 0); - result.put("distance", Math.sqrt(closestEntDistSqr)); - result.put("entity", new Box3JSEntity(closestEntity, server, engine)); - } else if (blockHit.getType() != HitResult.Type.MISS) { - Vec3 pos = blockHit.getLocation(); - result.put("hit", true); - result.put("x", pos.x); - result.put("y", pos.y); - result.put("z", pos.z); - Direction face = blockHit.getDirection(); - result.put("normalX", face.getStepX()); - result.put("normalY", face.getStepY()); - result.put("normalZ", face.getStepZ()); - result.put("distance", Math.sqrt(blockDistSqr)); - result.put("entity", null); - BlockPos bp = blockHit.getBlockPos(); - result.put("voxel", engine.getVoxelsBinding().getId(level.getBlockState(bp))); - } else { - result.put("hit", false); - } - return result; - } - - // ---- Explosion (MC extension) ---- - - public void explode(double x, double y, double z, float power) { - explode(x, y, z, power, false); - } - - public void explode(double x, double y, double z, float power, boolean fire) { - server.overworld().explode(null, x, y, z, power, fire, Level.ExplosionInteraction.BLOCK); - } - - // ---- Sound / Biome (MC extension) ---- - - public void playSoundAll(String path, double x, double y, double z, float volume, float pitch) { - ResourceLocation rl = ResourceLocation.tryParse(path); - if (rl == null) return; - var sound = BuiltInRegistries.SOUND_EVENT.getHolder(rl); - if (sound.isEmpty()) return; - var packet = new ClientboundSoundPacket(sound.get(), SoundSource.PLAYERS, x, y, z, volume, pitch, server.overworld().getRandom().nextLong()); - for (ServerPlayer sp : server.getPlayerList().getPlayers()) { - sp.connection.send(packet); - } - } - - public 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"); - } - // ---- Callback interfaces ---- @FunctionalInterface @@ -485,9 +392,17 @@ public interface EntityDamageCallback { void onDamage(Box3JSEntity entity, double amount, String source, Box3JSEntity attacker, long tick); } + @FunctionalInterface + public interface MessageCallback { + void onMessage(String from, Object data); + } + // ---- Namespace inner classes ---- - public class ScoreboardNS { + public static class ScoreboardNS { + private final MinecraftServer server; + ScoreboardNS(MinecraftServer server) { this.server = server; } + public void add(String name) { add(name, "dummy"); } public void add(String name, String criteria) { Scoreboard sb = server.getScoreboard(); @@ -538,23 +453,27 @@ public void remove(String name) { Objective obj = sb.getObjective(name); if (obj != null) sb.removeObjective(obj); } - public java.util.List> list(String objectiveName) { - java.util.List> result = new ArrayList<>(); + public java.util.List list(String objectiveName) { + java.util.List result = new ArrayList<>(); Scoreboard sb = server.getScoreboard(); Objective obj = sb.getObjective(objectiveName); if (obj == null) return result; for (ServerPlayer player : server.getPlayerList().getPlayers()) { int s = sb.getOrCreatePlayerScore(ScoreHolder.forNameOnly(player.getScoreboardName()), obj).get(); - Map m = new LinkedHashMap<>(); - m.put("name", player.getScoreboardName()); - m.put("value", s); + NativeObject m = new NativeObject(); + ScriptableObject.putProperty(m, "name", player.getScoreboardName()); + ScriptableObject.putProperty(m, "value", s); result.add(m); } return result; } } - public class BossBarNS { + public static class BossBarNS { + private final MinecraftServer server; + private final Map bossBars; + BossBarNS(MinecraftServer server, Map bossBars) { this.server = server; this.bossBars = bossBars; } + public void show(String name, String text, double progress, String colorName) { ServerBossEvent bar = bossBars.get(name); if (bar == null) { @@ -590,7 +509,10 @@ public void remove(String name) { } } - public class TeamNS { + public static class TeamNS { + private final MinecraftServer server; + TeamNS(MinecraftServer server) { this.server = server; } + public void create(String name, String colorName) { Scoreboard sb = server.getScoreboard(); if (sb.getPlayerTeam(name) != null) return; @@ -627,7 +549,10 @@ public String of(Object entityOrName) { } } - public class BorderNS { + public static class BorderNS { + private final MinecraftServer server; + BorderNS(MinecraftServer server) { this.server = server; } + public double size() { return server.overworld().getWorldBorder().getSize(); } public void center(double x, double z) { server.overworld().getWorldBorder().setCenter(x, z); } public void set(double size) { server.overworld().getWorldBorder().setSize(size); } @@ -639,7 +564,10 @@ public void shrink(double targetSize, double seconds) { public void warning(int blocks) { server.overworld().getWorldBorder().setWarningBlocks(blocks); } } - public class LightningNS { + public static class LightningNS { + private final MinecraftServer server; + LightningNS(MinecraftServer server) { this.server = server; } + public boolean strike(double x, double y, double z) { ServerLevel level = server.overworld(); LightningBolt bolt = EntityType.LIGHTNING_BOLT.create(level); @@ -661,7 +589,10 @@ public boolean strike(double x, double y, double z, double damage) { } } - public class FireworkNS { + public static class FireworkNS { + private final MinecraftServer server; + FireworkNS(MinecraftServer server) { this.server = server; } + public void launch(double x, double y, double z, String color, String shape) { ServerLevel level = server.overworld(); int colorInt = switch (color != null ? color.toLowerCase(Locale.ROOT) : "") { @@ -695,7 +626,10 @@ public void launch(double x, double y, double z, String color, String shape) { } } - public class ParticleNS { + public static class ParticleNS { + private final MinecraftServer server; + ParticleNS(MinecraftServer server) { this.server = server; } + public void spawn(String type, double x, double y, double z, int count, double dx, double dy, double dz, double speed) { var particle = resolveParticle(type); if (particle != null) { @@ -724,7 +658,10 @@ private ParticleOptions resolveParticle(String type) { } } - public class DropNS { + public static class DropNS { + private final MinecraftServer server; + DropNS(MinecraftServer server) { this.server = server; } + public void item(double x, double y, double z, String itemId, int count) { ServerLevel level = server.overworld(); ResourceLocation rl = ResourceLocation.tryParse(itemId); @@ -736,4 +673,135 @@ public void item(double x, double y, double z, String itemId, int count) { level.addFreshEntity(itemEntity); } } + + public static class QueryNS { + private final MinecraftServer server; + private final Box3ScriptEngine engine; + QueryNS(MinecraftServer server, Box3ScriptEngine engine) { this.server = server; this.engine = engine; } + + public Object raycast(GameVector3 origin, GameVector3 direction) { + return raycast(origin, direction, 5.0); + } + + public 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, 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, e -> 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; + } + + public 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, e -> true)) { + result.add(new Box3JSEntity(e, server, engine)); + } + return result; + } + + public String biome(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"); + } + } + + public static class EffectNS { + private final MinecraftServer server; + EffectNS(MinecraftServer server) { this.server = server; } + + public void explode(double x, double y, double z, double power) { + explode(x, y, 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 static class SoundNS { + private final MinecraftServer server; + SoundNS(MinecraftServer server) { this.server = server; } + + public void playAll(String path, double x, double y, double z, double volume, double pitch) { + ResourceLocation rl = ResourceLocation.tryParse(path); + if (rl == null) return; + var sound = BuiltInRegistries.SOUND_EVENT.getHolder(rl); + if (sound.isEmpty()) return; + var packet = new ClientboundSoundPacket(sound.get(), SoundSource.PLAYERS, x, y, z, (float) volume, (float) pitch, server.overworld().getRandom().nextLong()); + for (ServerPlayer sp : server.getPlayerList().getPlayers()) { + sp.connection.send(packet); + } + } + } + + public static class MessageNS { + private final Box3ScriptEngine engine; + MessageNS(Box3ScriptEngine engine) { this.engine = engine; } + + public void send(String target, Object data) { + engine.fireMessage(engine.getCurrentProject(), target, data); + } + + public void on(Function handler) { + String project = engine.getCurrentProject(); + if (project != null) { + engine.addMessageCallback(project, (from, d) -> engine.callFunction(handler, from, d)); + } + } + } } 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 index e4ecd328..00466755 100644 --- 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 @@ -3,6 +3,7 @@ import com.mojang.brigadier.arguments.StringArgumentType; import net.minecraft.commands.CommandSourceStack; import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; import net.neoforged.neoforge.event.RegisterCommandsEvent; import java.io.IOException; @@ -14,8 +15,6 @@ public class Box3ScriptCommand { - private static Path scriptDir; - public static void register(RegisterCommandsEvent event) { var dispatcher = event.getDispatcher(); @@ -47,7 +46,7 @@ public static void register(RegisterCommandsEvent event) { String input = StringArgumentType.getString(ctx, "path"); CommandSourceStack src = ctx.getSource(); var server = src.getServer(); - Path filePath = resolve(input); + Path filePath = resolve(input, server); if (!Files.exists(filePath)) { src.sendFailure(Component.literal("File not found: " + filePath)); return 0; @@ -65,6 +64,35 @@ public static void register(RegisterCommandsEvent event) { } return 1; }))) + // --- create --- + .then(literal("create") + .then(argument("name", StringArgumentType.word()) + .executes(ctx -> { + String name = StringArgumentType.getString(ctx, "name"); + Path projectDir = resolve(name, ctx.getSource().getServer()); + if (Files.exists(projectDir)) { + ctx.getSource().sendFailure( + Component.literal("Project already exists: " + name)); + return 0; + } + try { + Files.createDirectories(projectDir); + String template = "// " + name + " — Box3JS project\n" + + "world.onTick(() => {\n" + + " // 每 tick 执行\n" + + "});\n" + + "\n" + + "console.log('" + name + " loaded');\n"; + Files.writeString(projectDir.resolve("app.js"), template); + ctx.getSource().sendSuccess( + () -> Component.literal("Project created: " + name + + "\nUse /box3script on " + name + " to enable it."), + false); + } catch (IOException e) { + ctx.getSource().sendFailure(Component.literal("Failed to create: " + e.getMessage())); + } + return 1; + }))) // --- stop --- .then(literal("stop") .executes(ctx -> { @@ -137,7 +165,7 @@ public static void register(RegisterCommandsEvent event) { String project = StringArgumentType.getString(ctx, "project"); CommandSourceStack src = ctx.getSource(); var server = src.getServer(); - Path appJs = resolve(project).resolve("app.js"); + Path appJs = resolve(project, server).resolve("app.js"); if (!Files.exists(appJs)) { src.sendFailure(Component.literal("app.js not found: " + appJs)); return 0; @@ -158,12 +186,9 @@ public static void register(RegisterCommandsEvent event) { ); } - private static Path resolve(String input) { + private static Path resolve(String input, MinecraftServer server) { Path p = Path.of(input); if (p.isAbsolute()) return p; - if (scriptDir == null) { - scriptDir = Path.of("config", "box3", "script").toAbsolutePath(); - } - return scriptDir.resolve(input).normalize(); + return Box3ScriptConfig.get().getScriptDir(server).resolve(input).normalize(); } } 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 index 0688d864..33d66642 100644 --- 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 @@ -41,6 +41,8 @@ public class Box3ScriptEngine { private final List respawnCallbacks = new CopyOnWriteArrayList<>(); private final List blockActivateCallbacks = new CopyOnWriteArrayList<>(); private final List entityDamageCallbacks = new CopyOnWriteArrayList<>(); + private final Map> messageCallbacks = new ConcurrentHashMap<>(); + private String currentProject; private final Map voxelContactTracked = new ConcurrentHashMap<>(); private final Map fluidStateTracked = new ConcurrentHashMap<>(); private final Set entityContactPairs = ConcurrentHashMap.newKeySet(); @@ -59,50 +61,7 @@ public void init(MinecraftServer server) { this.worldBinding = new Box3JSWorld(server, this); this.voxelsBinding = new Box3JSVoxels(server); this.storageBinding = new Box3JSStorage(server.getServerDirectory().resolve("config"), this); - - Context cx = Context.enter(); - try { - scope = cx.initStandardObjects(); - - // World and console bindings - 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, "console", Context.javaToJS(new Box3JSConsole(), scope)); - - // sleep(ms) function - 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; - } - }); - - // Math types — NativeJavaClass wraps so `new GameVector3(x,y,z)` works - 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)); - - // Enums - 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(); - } - + setupScope(); initialized = true; } @@ -123,10 +82,13 @@ public void autoLoad(MinecraftServer server) { Path appJs = project.resolve("app.js"); if (Files.exists(appJs) && config.isEnabled(name)) { try { + setCurrentProject(name); eval(Files.readString(appJs)); Box3JS.LOGGER.info("Auto-loaded project: {}", name); } catch (Exception e) { Box3JS.LOGGER.error("Failed to auto-load: {}", appJs, e); + } finally { + setCurrentProject(null); } } }); @@ -143,8 +105,15 @@ public Object eval(String code) { } } - /** Tick callback from Box3JSWorld */ - public void addTickCallback(Runnable cb) { tickCallbacks.add(cb); } + /** Tick callback from Box3JSWorld — wraps to restore project context */ + public void addTickCallback(Runnable cb) { + String project = currentProject; + tickCallbacks.add(() -> { + String prev = currentProject; + setCurrentProject(project); + try { cb.run(); } finally { setCurrentProject(prev); } + }); + } public void addJoinCallback(Box3JSWorld.PlayerJoinCallback cb) { joinCallbacks.add(cb); } public void addLeaveCallback(Box3JSWorld.PlayerLeaveCallback cb) { leaveCallbacks.add(cb); } public void addVoxelDestroyCallback(Box3JSWorld.VoxelDestroyCallback cb) { voxelDestroyCallbacks.add(cb); } @@ -160,17 +129,49 @@ public Object eval(String code) { public void addRespawnCallback(Box3JSWorld.PlayerRespawnCallback cb) { respawnCallbacks.add(cb); } public void addBlockActivateCallback(Box3JSWorld.BlockActivateCallback cb) { blockActivateCallbacks.add(cb); } public void addEntityDamageCallback(Box3JSWorld.EntityDamageCallback cb) { entityDamageCallbacks.add(cb); } + public void addMessageCallback(String project, Box3JSWorld.MessageCallback cb) { + messageCallbacks.computeIfAbsent(project, k -> new CopyOnWriteArrayList<>()).add(cb); + } public void setPlayerChatHandler(UUID uuid, Function handler) { playerChatHandlers.put(uuid, handler); } + public void setCurrentProject(String name) { + currentProject = name; + worldBinding.setProjectName(name); + } + public String getCurrentProject() { return currentProject; } + + public void fireMessage(String sender, String target, Object data) { + if ("*".equals(target)) { + for (var entry : messageCallbacks.entrySet()) { + if (!entry.getKey().equals(sender)) { + for (var cb : entry.getValue()) { + String prev = currentProject; + setCurrentProject(entry.getKey()); + try { cb.onMessage(sender, data); } finally { setCurrentProject(prev); } + } + } + } + } else { + List cbs = messageCallbacks.get(target); + if (cbs != null) { + for (var cb : cbs) { + String prev = currentProject; + setCurrentProject(target); + try { cb.onMessage(sender, data); } finally { setCurrentProject(prev); } + } + } + } + } + public int scheduleTimeout(Function handler, int ticks) { int id = ++timerIdCounter; - timers.add(new TimerEntry(id, handler, ticks, 0)); + timers.add(new TimerEntry(id, handler, ticks, 0, currentProject)); return id; } public int scheduleInterval(Function handler, int ticks) { int id = ++timerIdCounter; - timers.add(new TimerEntry(id, handler, ticks, ticks)); + timers.add(new TimerEntry(id, handler, ticks, ticks, currentProject)); return id; } @@ -193,7 +194,9 @@ private void fireTimers() { } timers.removeAll(toRemove); for (var t : toFire) { - callFunction(t.handler); + String prev = currentProject; + setCurrentProject(t.project); + try { callFunction(t.handler); } finally { setCurrentProject(prev); } } } @@ -422,6 +425,7 @@ public void reset() { respawnCallbacks.clear(); blockActivateCallbacks.clear(); entityDamageCallbacks.clear(); + messageCallbacks.clear(); voxelContactTracked.clear(); fluidStateTracked.clear(); entityContactPairs.clear(); @@ -429,19 +433,36 @@ public void reset() { entityCustomProps.clear(); timers.clear(); timerIdCounter = 0; - Box3JSWorld freshWorld = new Box3JSWorld(server, this); - this.worldBinding = freshWorld; - Box3JSVoxels freshVoxels = new Box3JSVoxels(server); - this.voxelsBinding = freshVoxels; - Box3JSStorage freshStorage = new Box3JSStorage(server.getServerDirectory().resolve("config"), this); - this.storageBinding = freshStorage; + this.worldBinding = new Box3JSWorld(server, this); + this.voxelsBinding = new Box3JSVoxels(server); + 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(freshWorld, scope)); - ScriptableObject.putProperty(scope, "voxels", Context.javaToJS(freshVoxels, scope)); - ScriptableObject.putProperty(scope, "storage", Context.javaToJS(freshStorage, scope)); - ScriptableObject.putProperty(scope, "console", Context.javaToJS(new Box3JSConsole(), scope)); + 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, "sleep", new BaseFunction() { @Override public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { @@ -474,12 +495,27 @@ public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] ar public Box3JSWorld getWorldBinding() { return worldBinding; } public Box3JSVoxels getVoxelsBinding() { return voxelsBinding; } - public static class Box3JSConsole { - public void log(Object... args) { + 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.out.println("[Box3JS] " + sb.toString().trim()); + System.err.println("[Box3JS][ERROR] " + sb.toString().trim()); } + public void clear() { System.out.print("\033[H\033[2J"); System.out.flush(); @@ -491,12 +527,14 @@ private static class TimerEntry { final Function handler; int remaining; final int interval; + final String project; - TimerEntry(int id, Function handler, int remaining, int interval) { + 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/templates/META-INF/neoforge.mods.toml b/Box3JS-NeoForge-1.21.1/src/main/templates/META-INF/neoforge.mods.toml index b3e1b07d..b8a55996 100644 --- 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 @@ -46,7 +46,7 @@ authors="神岛实验室" # The description text for the mod (multi line!) (#mandatory) description=''' -Box3JS 模组为服务端引入 JavaScript 运行时,支持约 100 项 Box3 风格 API 与 Minecraft 原生扩展的补充,无需 Java 基础即可入门。 +Box3JS 为 Minecraft 服务端引入 JavaScript 脚本运行时,支持约 100 项 Box3 API 与 80 余项 Minecraft 扩展 API。无需 Java 基础即可编写服务端脚本。 ''' # The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. From c31a30e87707af47a7a80fe529edbbef9cea252d Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Thu, 30 Apr 2026 13:47:11 +0800 Subject: [PATCH 03/17] =?UTF-8?q?feat(api):=20=E6=96=B0=E5=A2=9E=E6=B8=B8?= =?UTF-8?q?=E6=88=8F=E8=A7=84=E5=88=99=E5=8F=8A=E6=89=A9=E5=B1=95=E4=B8=96?= =?UTF-8?q?=E7=95=8C=20API=20=E8=87=B3=20Box3=20API=20=E6=98=A0=E5=B0=84?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增游戏规则章节,包含 getGameRule/setGameRule API - 新增 world.onMessage 处理器,支持跨脚本通信 - 将项目间消息传递替换为以下完整章节: - 记分板 API - Boss条 API - 队伍 API - 世界边界 API - 闪电/烟花/粒子/掉落物 API - 爆炸/声音 API - 射线追踪/查询 API - 消息/命令 API - 整合并重新组织现有 API 文档 - 更新实体 API,增强效果和装备相关方法 - 为玩家新增物品/效果/属性/声音/维度 API - 新增 voxels.setSpawner API - 更新统计:MC 原生扩展从约 83 项增加至约 92 项 BREAKING CHANGE: 将实体命名空间 API 重构为直接方法调用 (effect.add -> addEffect,equipment.set -> setEquipment) --- .../META-INF/neoforge.mods.toml | 95 ++ .../docs/BOX3_API_MAPPING.md | 234 +++-- Box3JS-NeoForge-1.21.1/gradle.properties | 2 +- .../box3lab/box3js/script/Box3JSEntity.java | 129 ++- .../box3lab/box3js/script/Box3JSPlayer.java | 162 ++-- .../box3lab/box3js/script/Box3JSVoxels.java | 15 + .../box3lab/box3js/script/Box3JSWorld.java | 870 +++++++++--------- .../templates/META-INF/neoforge.mods.toml | 2 +- 8 files changed, 843 insertions(+), 666 deletions(-) create mode 100644 Box3JS-NeoForge-1.21.1/META-INF/neoforge.mods.toml 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/docs/BOX3_API_MAPPING.md b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_MAPPING.md index 2367ad42..acbb454c 100644 --- a/Box3JS-NeoForge-1.21.1/docs/BOX3_API_MAPPING.md +++ b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_MAPPING.md @@ -62,6 +62,13 @@ | `world.spawnPoint` | ⬆ | `ServerLevel.getSharedSpawnPos()` | 只读 GameVector3 | | `world.setWorldSpawn(pos)` | ⬆ | `ServerLevel.setDefaultSpawnPos()` | | +### 游戏规则 + +| API | 类型 | MC 映射 | 说明 | +|---|---|---|---| +| `world.getGameRule(name)` | ⬆ | `GameRules.getBoolean()` / `getInt()` | 支持规则名:doDaylightCycle, doWeatherCycle, keepInventory, doMobSpawning, doFireTick, mobGriefing, doImmediateRespawn | +| `world.setGameRule(name, value)` | ⬆ | `GameRule.set()` | value 为布尔值或数字字符串 | + ### 实体生成 | API | 类型 | MC 映射 | 说明 | @@ -88,6 +95,7 @@ | `world.onPlayerRespawn(handler)` | ⬆ | `(entity)` | | `world.onBlockActivate(handler)` | ⬆ | `(entity, x, y, z, voxel, tick)` | | `world.onEntityDamage(handler)` | ⬆ | `(entity, amount, source, attacker, tick)` | +| `world.onMessage(handler)` | ⬆ | `(from, data)` — 接收跨脚本消息 | ### 查询 / 聊天 @@ -106,25 +114,83 @@ | `world.clearTimeout(id)` | ⬆ | 取消 timeout | | `world.clearInterval(id)` | ⬆ | 取消 interval | -### 项目间消息传递(⬆ MC 扩展) +### 记分板(⬆ MC 扩展) -| API | 类型 | 说明 | -|---|---|---| -| `world.message.send(target, data)` | ⬆ | 精确路由到目标项目;`"*"` 广播给所有其他项目 | -| `world.message.on(handler)` | ⬆ | 接收其他项目发来的消息;`handler(from, data)` | +| API | 说明 | +|---|---| +| `world.addScoreboard(name)` | 创建 dummy 记分项 | +| `world.addScoreboard(name, criteria)` | 创建指定标准记分项 | +| `world.removeScoreboard(name)` | 删除记分项 | +| `world.setScore(entityOrName, obj, value)` | 设置分数 | +| `world.getScore(entityOrName, obj)` | 获取分数 | +| `world.showScoreboard(slot, obj)` | 显示记分板(slot: sidebar/list/belowname) | +| `world.hideScoreboard(slot)` | 清除显示槽位 | +| `world.listScores(obj)` | 获取所有分数条目 `[{name, value}]` | -### 命令 / 查询(⬆ MC 扩展) +### Boss 血条(⬆ MC 扩展) + +| API | 说明 | +|---|---| +| `world.showBossbar(name, text, progress, color)` | 显示/更新 Boss 血条 | +| `world.removeBossbar(name)` | 移除 Boss 血条 | + +### 队伍(⬆ MC 扩展) + +| API | 说明 | +|---|---| +| `world.createTeam(name, color)` | 创建队伍 | +| `world.removeTeam(name)` | 删除队伍 | +| `world.joinTeam(entity, teamName)` | 加入队伍 | +| `world.leaveTeam(entity)` | 移出队伍 | +| `world.getTeamOf(entity)` | 获取队伍名称 | + +### 世界边界(⬆ MC 扩展) + +| API | 说明 | +|---|---| +| `world.getBorderSize()` | 获取边界大小 | +| `world.setBorderCenter(x, z)` | 设置边界中心 | +| `world.setBorderSize(size)` | 立即设置边界大小 | +| `world.shrinkBorder(target, sec)` | 平滑缩圈 | +| `world.setBorderDamage(d)` | 边界外伤害 | +| `world.setBorderWarning(blocks)` | 警告距离 | + +### 闪电 / 烟花 / 粒子 / 掉落物(⬆ MC 扩展) + +| API | 说明 | +|---|---| +| `world.strikeLightning(x, y, z)` | 召唤闪电 | +| `world.strikeLightning(x, y, z, damage)` | 召唤闪电(自定义伤害) | +| `world.launchFirework(x, y, z, color, shape)` | 发射烟花 | +| `world.spawnParticle(type, x, y, z, count, dx, dy, dz, speed)` | 生成粒子 | +| `world.spawnParticleCircle(x, y, z, radius, type, count)` | 圆形粒子圈 | +| `world.dropItem(x, y, z, itemId, count)` | 掉落物品 | +| `world.launchProjectile(type, x, y, z, tx, ty, tz, speed)` | 发射抛射物(火球、箭等) | + +### 爆炸 / 音效(⬆ MC 扩展) + +| API | 说明 | +|---|---| +| `world.explode(x, y, z, power)` | 创建爆炸 | +| `world.explode(x, y, z, power, fire)` | 创建爆炸(可引火) | +| `world.playSound(path, x, y, z, vol, pitch)` | 在坐标播放音效给所有玩家 | + +### 射线 / 查询(⬆ MC 扩展) + +| API | 说明 | +|---|---| +| `world.raycast(origin, dir)` | 射线检测,默认 5 格,返回 `{hit, x, y, z, normalX, normalY, normalZ, distance, entity, voxel}` | +| `world.raycast(origin, dir, maxDist)` | 射线检测,自定义距离 | +| `world.entitiesInArea(pos1, pos2)` | 返回 AABB 区域内所有实体 | +| `world.getBiome(x, y, z)` | 获取生物群系命名空间 ID | + +### 消息 / 命令(⬆ MC 扩展) + +| API | 说明 | +|---|---| +| `world.sendMessage(target, data)` | 精确路由到目标项目;`"*"` 广播给所有其他项目 | +| `world.runCommand(cmd)` | 以服务器身份执行命令 | -| API | 类型 | 说明 | -|---|---|---| -| `world.runCommand(cmd)` | ⬆ | 以服务器身份执行命令 | -| `world.query.raycast(origin, dir)` | ⬆ | 射线检测,默认 5 格 | -| `world.query.raycast(origin, dir, dist)` | ⬆ | 射线检测,自定义距离 | -| `world.query.entitiesInArea(pos1, pos2)` | ⬆ | 返回 AABB 区域内所有实体 | -| `world.query.biome(x, y, z)` | ⬆ | 获取生物群系命名空间 ID | -| `world.effect.explode(x, y, z, power)` | ⬆ | 创建爆炸 | -| `world.effect.explode(x, y, z, power, fire)` | ⬆ | 创建爆炸(可引火) | -| `world.sound.playAll(path, x, y, z, vol, pitch)` | ⬆ | 在坐标播放音效给所有玩家 | --- ## entity (GameEntity) @@ -141,7 +207,6 @@ | `.addTag(tag)` | ✅ | | | `.hasTag(tag)` | ✅ | | | `.removeTag(tag)` | ✅ | | -| `.sound(path)` | ✅ | 固定 NOTE_BLOCK_PLING | | `.hp` | ✅ | LivingEntity 同步 | | `.maxHp` | ✅ | LivingEntity 同步 | | `.destroyed` | ✅ | `Entity.isRemoved()`,只读 | @@ -155,11 +220,16 @@ | `.lookAt(x, y, z)` | ⬆ | 实体面朝指定坐标 | | `.navigateTo(x, y, z, speed)` | ⬆ | 寻路步行到目标(PathfinderMob) | | `.setAI(enabled)` | ⬆ | 开关实体 AI | -| `.equipment.set(slot, itemId)` | ⬆ | 给生物穿装备;slot: mainhand/offhand/head/chest/legs/feet | -| `.effect.add(effectId, dur, amp)` | ⬆ | 给任意 LivingEntity 添加药水效果 | +| `.addEffect(id, dur, amp)` | ⬆ | 添加药水效果 | +| `.addEffect(id, dur, amp, hideParticles)` | ⬆ | 添加药水效果(可隐藏粒子) | +| `.setEquipment(slot, itemId)` | ⬆ | 给生物穿装备;slot: mainhand/offhand/head/chest/legs/feet | | `.setTarget(entity)` | ⬆ | 设置怪物攻击目标(Mob.setTarget) | | `.getTarget()` | ⬆ | 获取怪物当前攻击目标 | | `.clearTarget()` | ⬆ | 清除攻击目标 | +| `.setDropChance(slot, chance)` | ⬆ | 设置装备掉落率 0-1;slot 可为 "all" | +| `.getAttribute(id)` | ⬆ | 获取实体属性值,如 `minecraft:generic.attack_damage` | +| `.setAttribute(id, value)` | ⬆ | 设置实体属性基值 | +| `.setPersistent(v)` | ⬆ | 设为 true 时生物不会自然消失 | | `.isGlowing()` / `.setGlowing(v)` | ⬆ | 发光效果 | | `.getNameTag()` / `.setNameTag(n)` | ⬆ | 自定义名称 | | `.getOnGround()` | ⬆ | 是否在地面 | @@ -238,37 +308,35 @@ | `.link(href)` | ✅ | 可点击链接 | | `.onChat(handler)` | ✅ | 玩家级聊天回调 | -### 物品 / 效果 / 属性 - -| API | 类型 | 说明 | -|---|---|---| -| `.inventory.give(itemId, count)` | ⬆ | 命名空间 ID | -| `.effect.add(effectId, dur, amp)` | ⬆ | 命名空间 ID;duration 为 tick | -| `.effect.clear()` | ⬆ | `removeAllEffects()` | -| `.xp` | ⬆ | 经验等级 get/set | -| `.food` | ⬆ | 饱食度 get/set | -| `.saturation` | ⬆ | 饱和度 get/set | +### 物品(⬆ MC 扩展) -### 音效 - -| API | 类型 | 说明 | -|---|---|---| -| `.sound(path)` | ✅ | 固定 NOTE_BLOCK_PLING | -| `.sound.play(path, vol, pitch)` | ⬆ | 播放任意 MC 音效 | +| API | 说明 | +|---|---| +| `.giveItem(itemId, count)` | 给予物品,命名空间 ID | +| `.giveEnchantedItem(itemId, count, enchants)` | 给予附魔物品;enchants 为 `{enchant_id: level}` 对象 | +| `.giveNamedItem(itemId, count, name, lore)` | 给予带名称/描述的物品;lore 为字符串数组 | +| `.getHeldItem()` | 主手物品,返回 `{id, count}` | +| `.clearInventory()` | 清空背包 | -### 维度 / 物品(⬆ MC 扩展) +### 效果 / 属性(⬆ MC 扩展) -| API | 类型 | 说明 | -|---|---|---| -| `.dimension` | ⬆ | 维度 ID,get/set(set 可跨维度传送) | -| `.inventory.held()` | ⬆ | 主手物品 `{id, count}` | -| `.inventory.clear()` | ⬆ | 清空背包 | +| API | 说明 | +|---|---| +| `.addEffect(effectId, dur, amp)` | 添加药水效果,命名空间 ID;duration 为 tick | +| `.addEffect(effectId, dur, amp, hideParticles)` | 添加药水效果(可隐藏粒子) | +| `.clearEffects()` | 移除所有效果 | +| `.xp` | 经验等级 get/set | +| `.food` | 饱食度 get/set | +| `.saturation` | 饱和度 get/set | -### 命令(⬆ MC 扩展) +### 音效 / 维度(⬆ MC 扩展) -| API | 类型 | 说明 | -|---|---|---| -| `.runCommand(cmd)` | ⬆ | 以玩家身份执行命令 | +| API | 说明 | +|---|---| +| `.playSound(path, vol, pitch)` | 播放任意 MC 音效给该玩家 | +| `.dimension` | 维度 ID,get/set(set 可跨维度传送) | +| `.lookAt(x, y, z)` | 玩家面朝指定坐标 | +| `.runCommand(cmd)` | 以玩家身份执行命令 | --- @@ -302,6 +370,7 @@ | `voxels.getVoxelRotation(x,y,z)` | 0-3 | | `voxels.fillVoxel(x1,y1,z1, x2,y2,z2, voxel)` | ⬆ 填充矩形区域 | | `voxels.countVoxel(x1,y1,z1, x2,y2,z2, voxel)` | ⬆ 统计区域内匹配方块数量 | +| `voxels.setSpawner(x, y, z, entityType)` | ⬆ 设置刷怪笼刷出类型 | --- @@ -350,84 +419,11 @@ --- -## 命名空间 API (v2) — 全部为 ⬆ MC 扩展 - -以下 `world.*` / `player.*` / `entity.*` 命名空间 API 均为 MC 原生扩展,非 Box3 原有。 - -### world.scoreboard - -| API | 说明 | -|---|---| -| `world.scoreboard.add(name)` | 创建 dummy 记分项 | -| `world.scoreboard.add(name, criteria)` | 创建指定标准记分项 | -| `world.scoreboard.setScore(entityOrName, obj, value)` | 设置分数 | -| `world.scoreboard.getScore(entityOrName, obj)` | 获取分数 | -| `world.scoreboard.show(slot, obj)` | 显示记分板 | -| `world.scoreboard.hide(slot)` | 清除显示槽位 | -| `world.scoreboard.remove(name)` | 删除记分项 | -| `world.scoreboard.list(name)` | 获取所有分数条目 | - -### world.bossbar - -| API | 说明 | -|---|---| -| `world.bossbar.show(name, text, progress, color)` | 显示/更新 Boss 血条 | -| `world.bossbar.remove(name)` | 移除 Boss 血条 | - -### world.team - -| API | 说明 | -|---|---| -| `world.team.create(name, color)` | 创建队伍 | -| `world.team.join(entity, teamName)` | 加入队伍 | -| `world.team.leave(entity)` | 移出队伍 | -| `world.team.remove(name)` | 删除队伍 | -| `world.team.of(entity)` | 获取队伍名称 | - -### world.border - -| API | 说明 | -|---|---| -| `world.border.size()` | 获取边界大小 | -| `world.border.center(x, z)` | 设置边界中心 | -| `world.border.set(size)` | 立即设置边界大小 | -| `world.border.shrink(target, sec)` | 平滑缩圈 | -| `world.border.damage(d)` | 边界外伤害 | -| `world.border.warning(blocks)` | 警告距离 | - -### world.lightning - -| API | 说明 | -|---|---| -| `world.lightning.strike(x, y, z)` | 召唤闪电 | -| `world.lightning.strike(x, y, z, damage)` | 召唤闪电(自定义伤害) | - -### world.firework - -| API | 说明 | -|---|---| -| `world.firework.launch(x, y, z, color, shape)` | 发射烟花 | - -### world.particle - -| API | 说明 | -|---|---| -| `world.particle.spawn(type, x, y, z, ct, dx, dy, dz, spd)` | 生成粒子 | -| `world.particle.circle(x, y, z, radius, type, count)` | 圆形粒子圈 | - -### world.drop - -| API | 说明 | -|---|---| -| `world.drop.item(x, y, z, itemId, count)` | 掉落物品 | - ---- - ## 统计 | 状态 | 数量 | |---|---| | ✅ Box3 API | ~100 | -| ⬆ MC 扩展 | ~83 | +| ⬆ MC 扩展 | ~92 | > 最后更新:2026-04-30 diff --git a/Box3JS-NeoForge-1.21.1/gradle.properties b/Box3JS-NeoForge-1.21.1/gradle.properties index 91fa32fb..ebbdd051 100644 --- a/Box3JS-NeoForge-1.21.1/gradle.properties +++ b/Box3JS-NeoForge-1.21.1/gradle.properties @@ -32,7 +32,7 @@ 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 +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 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 index 667019aa..3063d056 100644 --- 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 @@ -5,7 +5,6 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; -import net.minecraft.sounds.SoundSource; import net.minecraft.world.effect.MobEffect; import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.world.entity.Entity; @@ -30,9 +29,6 @@ public class Box3JSEntity { private Function _onDestroyHandler; private final GameVector3 _position, _velocity, _bounds; - public final EffectNS effect; - public final EquipmentNS equipment; - public Box3JSEntity(Entity entity, MinecraftServer server, Box3ScriptEngine engine) { this.entity = entity; this.server = server; @@ -40,8 +36,6 @@ public Box3JSEntity(Entity entity, MinecraftServer server, Box3ScriptEngine engi 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(); - this.effect = new EffectNS(entity); - this.equipment = new EquipmentNS(entity); } public Entity getEntity() { return entity; } @@ -245,6 +239,77 @@ public void setAI(boolean 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) { + if (!(entity instanceof LivingEntity le)) return; + ResourceLocation rl = ResourceLocation.tryParse(effectId); + if (rl == null) return; + Holder effect = BuiltInRegistries.MOB_EFFECT.getHolder(rl).orElse(null); + if (effect == null) return; + le.addEffect(new MobEffectInstance(effect, duration, amplifier, false, !hideParticles, true)); + } + + // ---- Equipment (MC extension) ---- + + public void setEquipment(String slot, String itemId) { + if (!(entity instanceof Mob mob)) return; + EquipmentSlot equipmentSlot = parseEquipmentSlot(slot); + if (equipmentSlot == null) return; + ResourceLocation rl = ResourceLocation.tryParse(itemId); + if (rl == null) return; + Item item = BuiltInRegistries.ITEM.getOptional(rl).orElse(null); + if (item == null) return; + mob.setItemSlot(equipmentSlot, new ItemStack(item)); + } + + // ---- Drop chances (MC extension) ---- + + public void setDropChance(String slot, double chance) { + 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) { + if (entity instanceof Mob mob && v) mob.setPersistenceRequired(); + } + + // ---- Attributes (MC extension) ---- + + public double getAttribute(String attributeId) { + if (!(entity instanceof LivingEntity le)) return 0; + ResourceLocation rl = ResourceLocation.tryParse(attributeId); + if (rl == null) return 0; + var holder = BuiltInRegistries.ATTRIBUTE.getHolder(rl); + if (holder.isPresent()) return le.getAttributeValue(holder.get()); + return 0; + } + + public void setAttribute(String attributeId, double value) { + if (!(entity instanceof LivingEntity le)) return; + ResourceLocation rl = ResourceLocation.tryParse(attributeId); + if (rl == null) return; + var holder = BuiltInRegistries.ATTRIBUTE.getHolder(rl); + if (holder.isPresent()) { + var instance = le.getAttribute(holder.get()); + if (instance != null) instance.setBaseValue(value); + } + } + // ---- Lifecycle ---- public void destroy() { @@ -281,6 +346,18 @@ 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; @@ -294,44 +371,4 @@ public GameVector3 set(double x, double y, double z) { return this; } } - - // ---- Namespace classes ---- - - public static class EffectNS { - private final Entity entity; - EffectNS(Entity entity) { this.entity = entity; } - - public void add(String effectId, int duration, int amplifier) { - if (!(entity instanceof LivingEntity le)) return; - ResourceLocation rl = ResourceLocation.tryParse(effectId); - if (rl == null) return; - Holder effect = BuiltInRegistries.MOB_EFFECT.getHolder(rl).orElse(null); - if (effect == null) return; - le.addEffect(new MobEffectInstance(effect, duration, amplifier)); - } - } - - public static class EquipmentNS { - private final Entity entity; - EquipmentNS(Entity entity) { this.entity = entity; } - - public void set(String slot, String itemId) { - if (!(entity instanceof Mob mob)) return; - EquipmentSlot equipmentSlot = 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; - }; - if (equipmentSlot == null) return; - ResourceLocation rl = ResourceLocation.tryParse(itemId); - if (rl == null) return; - Item item = BuiltInRegistries.ITEM.getOptional(rl).orElse(null); - if (item == null) return; - mob.setItemSlot(equipmentSlot, new ItemStack(item)); - } - } } 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 index c77391c7..3db64b5a 100644 --- 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 @@ -33,15 +33,8 @@ public Box3JSPlayer(ServerPlayer player, MinecraftServer server, Box3ScriptEngin this.player = player; this.server = server; this.engine = engine; - this.inventory = new InventoryNS(player); - this.effect = new EffectNS(player); - this.sound = new SoundNS(player); } - public final InventoryNS inventory; - public final EffectNS effect; - public final SoundNS sound; - // ---- Info ---- public String getName() { return player.getGameProfile().getName(); } @@ -296,86 +289,123 @@ public void runCommand(String cmd) { public float getSaturation() { return player.getFoodData().getSaturationLevel(); } public void setSaturation(float v) { player.getFoodData().setSaturation(v); } - // ---- Custom properties ---- - - private Map props() { - return engine.getCustomProps(player.getUUID()); - } + // ---- Inventory ---- - @SuppressWarnings("unchecked") - private T getProp(String key, T defaultValue) { - Object v = props().get(key); - return v != null ? (T) v : defaultValue; + public void giveItem(String itemId, int count) { + ItemStack stack = makeItemStack(itemId, count, null); + if (stack != null) player.getInventory().add(stack); } - private void setProp(String key, Object value) { - props().put(key, value); + public void giveEnchantedItem(String itemId, int count, NativeObject enchants) { + ItemStack stack = makeItemStack(itemId, count, enchants); + if (stack != null) player.getInventory().add(stack); } - // ---- Namespace classes ---- - - public static class InventoryNS { - private final ServerPlayer player; - InventoryNS(ServerPlayer player) { this.player = player; } - - public void give(String itemId, int count) { - ResourceLocation rl = ResourceLocation.tryParse(itemId); - if (rl == null) return; - var item = BuiltInRegistries.ITEM.getOptional(rl); - if (item.isPresent()) { - ItemStack stack = new ItemStack(item.get(), Math.max(1, Math.min(count, 64))); - 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 held() { - 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()); + 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 clear() { - player.getInventory().clearContent(); - } + public void clearInventory() { + player.getInventory().clearContent(); } - public static class EffectNS { - private final ServerPlayer player; - EffectNS(ServerPlayer player) { this.player = player; } + // ---- Effects ---- - public void add(String effectId, int duration, int amplifier) { - ResourceLocation rl = ResourceLocation.tryParse(effectId); - if (rl == null) return; - var effect = BuiltInRegistries.MOB_EFFECT.getHolder(rl); - if (effect.isPresent()) { - player.addEffect(new MobEffectInstance(effect.get(), duration, amplifier)); - } + 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) { + ResourceLocation rl = ResourceLocation.tryParse(effectId); + if (rl == null) return; + var effect = BuiltInRegistries.MOB_EFFECT.getHolder(rl); + if (effect.isPresent()) { + player.addEffect(new MobEffectInstance(effect.get(), duration, amplifier, false, !hideParticles, true)); } + } + + public void clearEffects() { + player.removeAllEffects(); + } - public void clear() { - player.removeAllEffects(); + // ---- Sound ---- + + public void playSound(String path, double volume, double pitch) { + ResourceLocation rl = ResourceLocation.tryParse(path); + if (rl == null) return; + var sound = net.minecraft.core.registries.BuiltInRegistries.SOUND_EVENT.getOptional(rl); + if (sound.isPresent()) { + player.playNotifySound(sound.get(), net.minecraft.sounds.SoundSource.PLAYERS, (float) volume, (float) pitch); } } - public static class SoundNS { - private final ServerPlayer player; - SoundNS(ServerPlayer player) { this.player = player; } + // ---- Custom properties ---- + + 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); + } - public void play(String path, double volume, double pitch) { - ResourceLocation rl = ResourceLocation.tryParse(path); - if (rl == null) return; - var sound = net.minecraft.core.registries.BuiltInRegistries.SOUND_EVENT.getOptional(rl); - if (sound.isPresent()) { - player.playNotifySound(sound.get(), net.minecraft.sounds.SoundSource.PLAYERS, (float) volume, (float) pitch); + private ItemStack makeItemStack(String itemId, int count, NativeObject enchants) { + ResourceLocation rl = ResourceLocation.tryParse(itemId); + if (rl == null) return null; + var item = BuiltInRegistries.ITEM.getOptional(rl); + if (item.isEmpty()) return null; + ItemStack stack = new ItemStack(item.get(), 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/Box3JSVoxels.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSVoxels.java index 08b847dd..6eda0fc6 100644 --- 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 @@ -225,6 +225,21 @@ public String getVoxelName(int x, int y, int z) { return BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(); } + /** 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; + + ResourceLocation rl = ResourceLocation.tryParse(entityType); + if (rl == null) return; + var opt = BuiltInRegistries.ENTITY_TYPE.getOptional(rl); + if (opt.isEmpty()) return; + + spawnerBe.setEntityId(opt.get(), level.getRandom()); + } + /** getVoxelRotation(x, y, z): number — 0, 1, 2, 3 */ public int getVoxelRotation(int x, int y, int z) { ServerLevel level = server.overworld(); 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 index 8adb51c8..de5d0769 100644 --- 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 @@ -59,34 +59,9 @@ public class Box3JSWorld { private String projectName; private final Map bossBars = new HashMap<>(); - public final ScoreboardNS scoreboard; - public final BossBarNS bossbar; - public final TeamNS team; - public final BorderNS border; - public final LightningNS lightning; - public final FireworkNS firework; - public final ParticleNS particle; - public final DropNS drop; - public final QueryNS query; - public final EffectNS effect; - public final SoundNS sound; - public final MessageNS message; - public Box3JSWorld(MinecraftServer server, Box3ScriptEngine engine) { this.server = server; this.engine = engine; - this.scoreboard = new ScoreboardNS(server); - this.bossbar = new BossBarNS(server, bossBars); - this.team = new TeamNS(server); - this.border = new BorderNS(server); - this.lightning = new LightningNS(server); - this.firework = new FireworkNS(server); - this.particle = new ParticleNS(server); - this.drop = new DropNS(server); - this.query = new QueryNS(server, engine); - this.effect = new EffectNS(server); - this.sound = new SoundNS(server); - this.message = new MessageNS(engine); } public void setProjectName(String name) { this.projectName = name; } @@ -144,6 +119,46 @@ public void setDifficulty(Object v) { if (diff != null) server.setDifficulty(diff, true); } + // ---- Game Rules (MC extension) ---- + + public Object getGameRule(String name) { + GameRules rules = server.overworld().getGameRules(); + switch (name) { + case "doDaylightCycle": return rules.getBoolean(GameRules.RULE_DAYLIGHT); + case "doWeatherCycle": return rules.getBoolean(GameRules.RULE_WEATHER_CYCLE); + case "keepInventory": return rules.getBoolean(GameRules.RULE_KEEPINVENTORY); + case "doMobSpawning": return rules.getBoolean(GameRules.RULE_DOMOBSPAWNING); + case "doFireTick": return rules.getBoolean(GameRules.RULE_DOFIRETICK); + case "mobGriefing": return rules.getBoolean(GameRules.RULE_MOBGRIEFING); + case "doImmediateRespawn": return rules.getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN); + default: return null; + } + } + + public void setGameRule(String name, Object value) { + GameRules rules = server.overworld().getGameRules(); + switch (name) { + case "doDaylightCycle": + rules.getRule(GameRules.RULE_DAYLIGHT).set(coerceBool(value), server); break; + case "doWeatherCycle": + rules.getRule(GameRules.RULE_WEATHER_CYCLE).set(coerceBool(value), server); break; + case "keepInventory": + rules.getRule(GameRules.RULE_KEEPINVENTORY).set(coerceBool(value), server); break; + case "doMobSpawning": + rules.getRule(GameRules.RULE_DOMOBSPAWNING).set(coerceBool(value), server); break; + case "doFireTick": + rules.getRule(GameRules.RULE_DOFIRETICK).set(coerceBool(value), server); break; + case "mobGriefing": + rules.getRule(GameRules.RULE_MOBGRIEFING).set(coerceBool(value), server); break; + case "doImmediateRespawn": + rules.getRule(GameRules.RULE_DO_IMMEDIATE_RESPAWN).set(coerceBool(value), server); break; + } + } + + private static boolean coerceBool(Object v) { + return v instanceof Boolean b ? b : Boolean.parseBoolean(v.toString()); + } + // ---- Spawn ---- public GameVector3 getSpawnPoint() { @@ -247,6 +262,13 @@ public void onEntityDamage(Function handler) { 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) { @@ -308,6 +330,396 @@ public void runCommand(String cmd) { server.getCommands().performPrefixedCommand(source, cmd); } + // ---- Scoreboard ---- + + public void addScoreboard(String name) { addScoreboard(name, "dummy"); } + public void addScoreboard(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); + } + public void removeScoreboard(String name) { + Scoreboard sb = server.getScoreboard(); + Objective obj = sb.getObjective(name); + if (obj != null) sb.removeObjective(obj); + } + public void setScore(Object entityOrName, String objectiveName, int value) { + Scoreboard sb = server.getScoreboard(); + Objective obj = sb.getObjective(objectiveName); + if (obj == null) return; + String name = resolveScoreName(entityOrName); + if (name == null) return; + sb.getOrCreatePlayerScore(ScoreHolder.forNameOnly(name), obj).set(value); + } + public int getScore(Object entityOrName, String objectiveName) { + Scoreboard sb = server.getScoreboard(); + Objective obj = sb.getObjective(objectiveName); + if (obj == null) return 0; + String name = resolveScoreName(entityOrName); + if (name == null) return 0; + ScoreAccess access = sb.getOrCreatePlayerScore(ScoreHolder.forNameOnly(name), obj); + return access.get(); + } + public void showScoreboard(String slot, String objectiveName) { + Scoreboard sb = server.getScoreboard(); + DisplaySlot displaySlot = switch (slot.toLowerCase()) { + case "list" -> DisplaySlot.LIST; + case "belowname", "below_name" -> DisplaySlot.BELOW_NAME; + default -> DisplaySlot.SIDEBAR; + }; + Objective obj = sb.getObjective(objectiveName); + sb.setDisplayObjective(displaySlot, obj); + } + public void hideScoreboard(String slot) { + Scoreboard sb = server.getScoreboard(); + DisplaySlot displaySlot = switch (slot.toLowerCase()) { + case "list" -> DisplaySlot.LIST; + case "belowname", "below_name" -> DisplaySlot.BELOW_NAME; + default -> DisplaySlot.SIDEBAR; + }; + sb.setDisplayObjective(displaySlot, null); + } + public java.util.List listScores(String objectiveName) { + java.util.List result = new ArrayList<>(); + Scoreboard sb = server.getScoreboard(); + Objective obj = sb.getObjective(objectiveName); + if (obj == null) return result; + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + int s = sb.getOrCreatePlayerScore(ScoreHolder.forNameOnly(player.getScoreboardName()), obj).get(); + NativeObject m = new NativeObject(); + ScriptableObject.putProperty(m, "name", player.getScoreboardName()); + ScriptableObject.putProperty(m, "value", s); + result.add(m); + } + return result; + } + + // ---- Boss Bar ---- + + public void showBossbar(String name, String text, double progress, String colorName) { + ServerBossEvent bar = bossBars.get(name); + if (bar == null) { + BossBarColor color = colorName == null ? BossBarColor.WHITE : 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; + }; + bar = new ServerBossEvent(Component.literal(text), color, BossBarOverlay.PROGRESS); + bossBars.put(name, bar); + } else { + bar.setName(Component.literal(text)); + if (colorName != null) bar.setColor(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; + }); + } + bar.setProgress((float) Math.max(0, Math.min(1, progress))); + for (ServerPlayer sp : server.getPlayerList().getPlayers()) bar.addPlayer(sp); + } + public void removeBossbar(String name) { + ServerBossEvent bar = bossBars.remove(name); + if (bar != null) bar.removeAllPlayers(); + } + + // ---- Team ---- + + public void createTeam(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)); + } + } + public void removeTeam(String name) { + Scoreboard sb = server.getScoreboard(); + PlayerTeam team = sb.getPlayerTeam(name); + if (team != null) sb.removePlayerTeam(team); + } + public void joinTeam(Object entityOrName, String teamName) { + Scoreboard sb = server.getScoreboard(); + PlayerTeam team = sb.getPlayerTeam(teamName); + if (team == null) return; + String name = resolveScoreName(entityOrName); + if (name != null) sb.addPlayerToTeam(name, team); + } + public void leaveTeam(Object entityOrName) { + Scoreboard sb = server.getScoreboard(); + String name = resolveScoreName(entityOrName); + if (name != null) sb.removePlayerFromTeam(name); + } + public String getTeamOf(Object entityOrName) { + Scoreboard sb = server.getScoreboard(); + String name = resolveScoreName(entityOrName); + if (name == null) return null; + PlayerTeam team = sb.getPlayersTeam(name); + return team != null ? team.getName() : null; + } + + // ---- World Border ---- + + public double getBorderSize() { return server.overworld().getWorldBorder().getSize(); } + public void setBorderCenter(double x, double z) { server.overworld().getWorldBorder().setCenter(x, z); } + public void setBorderSize(double size) { server.overworld().getWorldBorder().setSize(size); } + public void shrinkBorder(double targetSize, double seconds) { + WorldBorder border = server.overworld().getWorldBorder(); + border.lerpSizeBetween(border.getSize(), targetSize, (long)(seconds * 1000)); + } + public void setBorderDamage(double damage) { server.overworld().getWorldBorder().setDamagePerBlock(damage); } + public void setBorderWarning(int blocks) { server.overworld().getWorldBorder().setWarningBlocks(blocks); } + + // ---- Lightning ---- + + public boolean strikeLightning(double x, double y, double z) { + ServerLevel level = server.overworld(); + LightningBolt bolt = EntityType.LIGHTNING_BOLT.create(level); + if (bolt == null) return false; + bolt.moveTo(x, y, z); + bolt.setVisualOnly(false); + level.addFreshEntity(bolt); + return true; + } + public boolean strikeLightning(double x, double y, double z, double damage) { + ServerLevel level = server.overworld(); + LightningBolt bolt = EntityType.LIGHTNING_BOLT.create(level); + if (bolt == null) return false; + bolt.moveTo(x, y, z); + bolt.setDamage((float) damage); + bolt.setVisualOnly(false); + level.addFreshEntity(bolt); + return true; + } + + // ---- Projectile (MC extension) ---- + + public Box3JSEntity launchProjectile(String type, double x, double y, double z, + double tx, double ty, double tz, double speed) { + ServerLevel level = server.overworld(); + ResourceLocation rl = ResourceLocation.tryParse(type); + if (rl == null) return null; + var opt = BuiltInRegistries.ENTITY_TYPE.getOptional(rl); + if (opt.isEmpty()) return null; + Entity entity = opt.get().create(level); + 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); + } + + level.addFreshEntity(entity); + return new Box3JSEntity(entity, server, engine); + } + + // ---- Firework ---- + + public void launchFirework(double x, double y, double z, String color, String shape) { + ServerLevel level = server.overworld(); + int colorInt = switch (color != null ? color.toLowerCase(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(level, x, y, z, rocket); + level.addFreshEntity(entity); + } + + // ---- Particle ---- + + public void spawnParticle(String type, double x, double y, double z, int count, double dx, double dy, double dz, double speed) { + var particle = resolveParticle(type); + if (particle != null) { + server.overworld().sendParticles(particle, x, y, z, count, dx, dy, dz, speed); + } + } + public void spawnParticleCircle(double x, double y, double z, double radius, String type, int count) { + var particle = resolveParticle(type); + if (particle == null) return; + ServerLevel level = server.overworld(); + for (int i = 0; i < count; i++) { + double angle = (2.0 * Math.PI * i) / count; + double px = x + Math.cos(angle) * radius; + double pz = z + Math.sin(angle) * radius; + level.sendParticles(particle, px, y, pz, 1, 0, 0, 0, 0); + } + } + private ParticleOptions resolveParticle(String type) { + ResourceLocation rl = ResourceLocation.tryParse(type); + if (rl == null) return null; + var particle = BuiltInRegistries.PARTICLE_TYPE.getOptional(rl); + if (particle.isEmpty()) return null; + var p = particle.get(); + if (p instanceof ParticleOptions options) return options; + return null; + } + + // ---- Drop Item ---- + + public void dropItem(double x, double y, double z, String itemId, int count) { + ServerLevel level = server.overworld(); + ResourceLocation rl = ResourceLocation.tryParse(itemId); + if (rl == null) return; + var item = BuiltInRegistries.ITEM.getOptional(rl).orElse(null); + if (item == null) return; + ItemStack stack = new ItemStack(item, Math.max(1, count)); + ItemEntity itemEntity = new ItemEntity(level, x, y, z, stack); + level.addFreshEntity(itemEntity); + } + + // ---- Query ---- + + public Object raycast(GameVector3 origin, GameVector3 direction) { + return raycast(origin, direction, 5.0); + } + + public 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, 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, e -> 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; + } + + public 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, e -> true)) { + result.add(new Box3JSEntity(e, server, engine)); + } + return result; + } + + public 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"); + } + + // ---- Explode ---- + + public void explode(double x, double y, double z, double power) { + explode(x, y, 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); + } + + // ---- Sound ---- + + public void playSound(String path, double x, double y, double z, double volume, double pitch) { + ResourceLocation rl = ResourceLocation.tryParse(path); + if (rl == null) return; + var sound = BuiltInRegistries.SOUND_EVENT.getHolder(rl); + if (sound.isEmpty()) return; + var packet = new ClientboundSoundPacket(sound.get(), SoundSource.PLAYERS, x, y, z, (float) volume, (float) pitch, server.overworld().getRandom().nextLong()); + for (ServerPlayer sp : server.getPlayerList().getPlayers()) { + sp.connection.send(packet); + } + } + + // ---- Message ---- + + public void sendMessage(String target, Object data) { + engine.fireMessage(engine.getCurrentProject(), target, data); + } + + // ---- Helpers ---- + private static String resolveScoreName(Object entityOrName) { if (entityOrName instanceof String s) return s; if (entityOrName instanceof Box3JSEntity e) return e.getEntity().getScoreboardName(); @@ -396,412 +808,4 @@ public interface EntityDamageCallback { public interface MessageCallback { void onMessage(String from, Object data); } - - // ---- Namespace inner classes ---- - - public static class ScoreboardNS { - private final MinecraftServer server; - ScoreboardNS(MinecraftServer server) { this.server = server; } - - public void add(String name) { add(name, "dummy"); } - public void add(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); - } - public void setScore(Object entityOrName, String objectiveName, int value) { - Scoreboard sb = server.getScoreboard(); - Objective obj = sb.getObjective(objectiveName); - if (obj == null) return; - String name = resolveScoreName(entityOrName); - if (name == null) return; - sb.getOrCreatePlayerScore(ScoreHolder.forNameOnly(name), obj).set(value); - } - public int getScore(Object entityOrName, String objectiveName) { - Scoreboard sb = server.getScoreboard(); - Objective obj = sb.getObjective(objectiveName); - if (obj == null) return 0; - String name = resolveScoreName(entityOrName); - if (name == null) return 0; - ScoreAccess access = sb.getOrCreatePlayerScore(ScoreHolder.forNameOnly(name), obj); - return access.get(); - } - public void show(String slot, String objectiveName) { - Scoreboard sb = server.getScoreboard(); - DisplaySlot displaySlot = switch (slot.toLowerCase()) { - case "list" -> DisplaySlot.LIST; - case "belowname", "below_name" -> DisplaySlot.BELOW_NAME; - default -> DisplaySlot.SIDEBAR; - }; - Objective obj = sb.getObjective(objectiveName); - sb.setDisplayObjective(displaySlot, obj); - } - public void hide(String slot) { - Scoreboard sb = server.getScoreboard(); - DisplaySlot displaySlot = switch (slot.toLowerCase()) { - case "list" -> DisplaySlot.LIST; - case "belowname", "below_name" -> DisplaySlot.BELOW_NAME; - default -> DisplaySlot.SIDEBAR; - }; - sb.setDisplayObjective(displaySlot, null); - } - public void remove(String name) { - Scoreboard sb = server.getScoreboard(); - Objective obj = sb.getObjective(name); - if (obj != null) sb.removeObjective(obj); - } - public java.util.List list(String objectiveName) { - java.util.List result = new ArrayList<>(); - Scoreboard sb = server.getScoreboard(); - Objective obj = sb.getObjective(objectiveName); - if (obj == null) return result; - for (ServerPlayer player : server.getPlayerList().getPlayers()) { - int s = sb.getOrCreatePlayerScore(ScoreHolder.forNameOnly(player.getScoreboardName()), obj).get(); - NativeObject m = new NativeObject(); - ScriptableObject.putProperty(m, "name", player.getScoreboardName()); - ScriptableObject.putProperty(m, "value", s); - result.add(m); - } - return result; - } - } - - public static class BossBarNS { - private final MinecraftServer server; - private final Map bossBars; - BossBarNS(MinecraftServer server, Map bossBars) { this.server = server; this.bossBars = bossBars; } - - public void show(String name, String text, double progress, String colorName) { - ServerBossEvent bar = bossBars.get(name); - if (bar == null) { - BossBarColor color = colorName == null ? BossBarColor.WHITE : 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; - }; - bar = new ServerBossEvent(Component.literal(text), color, BossBarOverlay.PROGRESS); - bossBars.put(name, bar); - } else { - bar.setName(Component.literal(text)); - if (colorName != null) bar.setColor(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; - }); - } - bar.setProgress((float) Math.max(0, Math.min(1, progress))); - for (ServerPlayer sp : server.getPlayerList().getPlayers()) bar.addPlayer(sp); - } - public void remove(String name) { - ServerBossEvent bar = bossBars.remove(name); - if (bar != null) bar.removeAllPlayers(); - } - } - - public static class TeamNS { - private final MinecraftServer server; - TeamNS(MinecraftServer server) { this.server = server; } - - public void create(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)); - } - } - public void join(Object entityOrName, String teamName) { - Scoreboard sb = server.getScoreboard(); - PlayerTeam team = sb.getPlayerTeam(teamName); - if (team == null) return; - String name = resolveScoreName(entityOrName); - if (name != null) sb.addPlayerToTeam(name, team); - } - public void leave(Object entityOrName) { - Scoreboard sb = server.getScoreboard(); - String name = resolveScoreName(entityOrName); - if (name != null) sb.removePlayerFromTeam(name); - } - public void remove(String name) { - Scoreboard sb = server.getScoreboard(); - PlayerTeam team = sb.getPlayerTeam(name); - if (team != null) sb.removePlayerTeam(team); - } - public String of(Object entityOrName) { - Scoreboard sb = server.getScoreboard(); - String name = resolveScoreName(entityOrName); - if (name == null) return null; - PlayerTeam team = sb.getPlayersTeam(name); - return team != null ? team.getName() : null; - } - } - - public static class BorderNS { - private final MinecraftServer server; - BorderNS(MinecraftServer server) { this.server = server; } - - public double size() { return server.overworld().getWorldBorder().getSize(); } - public void center(double x, double z) { server.overworld().getWorldBorder().setCenter(x, z); } - public void set(double size) { server.overworld().getWorldBorder().setSize(size); } - public void shrink(double targetSize, double seconds) { - WorldBorder border = server.overworld().getWorldBorder(); - border.lerpSizeBetween(border.getSize(), targetSize, (long)(seconds * 1000)); - } - public void damage(double damage) { server.overworld().getWorldBorder().setDamagePerBlock(damage); } - public void warning(int blocks) { server.overworld().getWorldBorder().setWarningBlocks(blocks); } - } - - public static class LightningNS { - private final MinecraftServer server; - LightningNS(MinecraftServer server) { this.server = server; } - - public boolean strike(double x, double y, double z) { - ServerLevel level = server.overworld(); - LightningBolt bolt = EntityType.LIGHTNING_BOLT.create(level); - if (bolt == null) return false; - bolt.moveTo(x, y, z); - bolt.setVisualOnly(false); - level.addFreshEntity(bolt); - return true; - } - public boolean strike(double x, double y, double z, double damage) { - ServerLevel level = server.overworld(); - LightningBolt bolt = EntityType.LIGHTNING_BOLT.create(level); - if (bolt == null) return false; - bolt.moveTo(x, y, z); - bolt.setDamage((float) damage); - bolt.setVisualOnly(false); - level.addFreshEntity(bolt); - return true; - } - } - - public static class FireworkNS { - private final MinecraftServer server; - FireworkNS(MinecraftServer server) { this.server = server; } - - public void launch(double x, double y, double z, String color, String shape) { - ServerLevel level = server.overworld(); - int colorInt = switch (color != null ? color.toLowerCase(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(level, x, y, z, rocket); - level.addFreshEntity(entity); - } - } - - public static class ParticleNS { - private final MinecraftServer server; - ParticleNS(MinecraftServer server) { this.server = server; } - - public void spawn(String type, double x, double y, double z, int count, double dx, double dy, double dz, double speed) { - var particle = resolveParticle(type); - if (particle != null) { - server.overworld().sendParticles(particle, x, y, z, count, dx, dy, dz, speed); - } - } - public void circle(double x, double y, double z, double radius, String type, int count) { - var particle = resolveParticle(type); - if (particle == null) return; - ServerLevel level = server.overworld(); - for (int i = 0; i < count; i++) { - double angle = (2.0 * Math.PI * i) / count; - double px = x + Math.cos(angle) * radius; - double pz = z + Math.sin(angle) * radius; - level.sendParticles(particle, px, y, pz, 1, 0, 0, 0, 0); - } - } - private ParticleOptions resolveParticle(String type) { - ResourceLocation rl = ResourceLocation.tryParse(type); - if (rl == null) return null; - var particle = BuiltInRegistries.PARTICLE_TYPE.getOptional(rl); - if (particle.isEmpty()) return null; - var p = particle.get(); - if (p instanceof ParticleOptions options) return options; - return null; - } - } - - public static class DropNS { - private final MinecraftServer server; - DropNS(MinecraftServer server) { this.server = server; } - - public void item(double x, double y, double z, String itemId, int count) { - ServerLevel level = server.overworld(); - ResourceLocation rl = ResourceLocation.tryParse(itemId); - if (rl == null) return; - var item = BuiltInRegistries.ITEM.getOptional(rl).orElse(null); - if (item == null) return; - ItemStack stack = new ItemStack(item, Math.max(1, count)); - ItemEntity itemEntity = new ItemEntity(level, x, y, z, stack); - level.addFreshEntity(itemEntity); - } - } - - public static class QueryNS { - private final MinecraftServer server; - private final Box3ScriptEngine engine; - QueryNS(MinecraftServer server, Box3ScriptEngine engine) { this.server = server; this.engine = engine; } - - public Object raycast(GameVector3 origin, GameVector3 direction) { - return raycast(origin, direction, 5.0); - } - - public 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, 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, e -> 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; - } - - public 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, e -> true)) { - result.add(new Box3JSEntity(e, server, engine)); - } - return result; - } - - public String biome(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"); - } - } - - public static class EffectNS { - private final MinecraftServer server; - EffectNS(MinecraftServer server) { this.server = server; } - - public void explode(double x, double y, double z, double power) { - explode(x, y, 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 static class SoundNS { - private final MinecraftServer server; - SoundNS(MinecraftServer server) { this.server = server; } - - public void playAll(String path, double x, double y, double z, double volume, double pitch) { - ResourceLocation rl = ResourceLocation.tryParse(path); - if (rl == null) return; - var sound = BuiltInRegistries.SOUND_EVENT.getHolder(rl); - if (sound.isEmpty()) return; - var packet = new ClientboundSoundPacket(sound.get(), SoundSource.PLAYERS, x, y, z, (float) volume, (float) pitch, server.overworld().getRandom().nextLong()); - for (ServerPlayer sp : server.getPlayerList().getPlayers()) { - sp.connection.send(packet); - } - } - } - - public static class MessageNS { - private final Box3ScriptEngine engine; - MessageNS(Box3ScriptEngine engine) { this.engine = engine; } - - public void send(String target, Object data) { - engine.fireMessage(engine.getCurrentProject(), target, data); - } - - public void on(Function handler) { - String project = engine.getCurrentProject(); - if (project != null) { - engine.addMessageCallback(project, (from, d) -> engine.callFunction(handler, from, d)); - } - } - } } 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 index b8a55996..526db5a3 100644 --- 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 @@ -46,7 +46,7 @@ authors="神岛实验室" # The description text for the mod (multi line!) (#mandatory) description=''' -Box3JS 为 Minecraft 服务端引入 JavaScript 脚本运行时,支持约 100 项 Box3 API 与 80 余项 Minecraft 扩展 API。无需 Java 基础即可编写服务端脚本。 +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. From fe88a614f97174c0e965a3e820dfa92b19b01541 Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Thu, 30 Apr 2026 14:09:14 +0800 Subject: [PATCH 04/17] =?UTF-8?q?feat(commands):=20=E4=B8=BA=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E5=91=BD=E4=BB=A4=E6=B6=88=E6=81=AF=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=9B=BD=E9=99=85=E5=8C=96=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除主类中未使用的 ModConfig 导入和注册 - 删除仅处理配置规范的空白 Config 类 - 为所有脚本命令反馈消息添加语言支持 - 将硬编码的文字组件替换为可翻译组件 - 在 en_us.json 中为多种命令响应添加新的语言键 - 为项目列表中的启用/禁用标签实现多语言支持 - 添加一次性批量启用/禁用所有项目的功能 --- .../main/java/com/box3lab/box3js/Box3JS.java | 3 - .../main/java/com/box3lab/box3js/Config.java | 10 --- .../box3js/script/Box3ScriptCommand.java | 70 +++++++++++++------ .../box3js/script/Box3ScriptConfig.java | 5 ++ .../resources/assets/box3js/lang/en_us.json | 32 ++++++--- .../resources/assets/box3js/lang/zh_cn.json | 23 ++++++ 6 files changed, 96 insertions(+), 47 deletions(-) delete mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Config.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/zh_cn.json 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 index 7ce2a17c..1bc43438 100644 --- 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 @@ -7,7 +7,6 @@ import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.Mod; -import net.neoforged.fml.config.ModConfig; import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.event.ServerChatEvent; import net.neoforged.neoforge.event.entity.living.LivingDamageEvent; @@ -26,8 +25,6 @@ public class Box3JS { public static final Logger LOGGER = LogUtils.getLogger(); public Box3JS(IEventBus modEventBus, ModContainer modContainer) { - modContainer.registerConfig(ModConfig.Type.COMMON, Config.SPEC); - // Script commands NeoForge.EVENT_BUS.addListener(Box3ScriptCommand::register); diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Config.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Config.java deleted file mode 100644 index ab99b7f7..00000000 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Config.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.box3lab.box3js; - -import net.neoforged.fml.event.config.ModConfigEvent; -import net.neoforged.neoforge.common.ModConfigSpec; - -public class Config { - private static final ModConfigSpec.Builder BUILDER = new ModConfigSpec.Builder(); - - static final ModConfigSpec SPEC = BUILDER.build(); -} 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 index 00466755..3f67056b 100644 --- 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 @@ -2,6 +2,7 @@ import com.mojang.brigadier.arguments.StringArgumentType; import net.minecraft.commands.CommandSourceStack; +import net.minecraft.locale.Language; import net.minecraft.network.chat.Component; import net.minecraft.server.MinecraftServer; import net.neoforged.neoforge.event.RegisterCommandsEvent; @@ -15,6 +16,8 @@ public class Box3ScriptCommand { + private static final String I = "box3js.command."; + public static void register(RegisterCommandsEvent event) { var dispatcher = event.getDispatcher(); @@ -31,10 +34,10 @@ public static void register(RegisterCommandsEvent event) { Box3ScriptEngine.get().init(server); Box3ScriptEngine.get().eval(code); ctx.getSource().sendSuccess( - () -> Component.literal("Script executed."), false); + () -> Component.translatable(I + "eval.success"), false); } catch (Exception e) { ctx.getSource().sendFailure( - Component.literal("Script error: " + e.getMessage())); + Component.translatable(I + "error", e.getMessage())); e.printStackTrace(); } return 1; @@ -48,18 +51,18 @@ public static void register(RegisterCommandsEvent event) { var server = src.getServer(); Path filePath = resolve(input, server); if (!Files.exists(filePath)) { - src.sendFailure(Component.literal("File not found: " + filePath)); + src.sendFailure(Component.translatable(I + "file.not_found", filePath)); return 0; } try { Box3ScriptEngine.get().init(server); Box3ScriptEngine.get().eval(Files.readString(filePath)); src.sendSuccess( - () -> Component.literal("Executed: " + filePath.getFileName()), false); + () -> Component.translatable(I + "file.executed", filePath.getFileName()), false); } catch (IOException e) { - src.sendFailure(Component.literal("Failed to read file: " + e.getMessage())); + src.sendFailure(Component.translatable(I + "file.read_error", e.getMessage())); } catch (Exception e) { - src.sendFailure(Component.literal("Script error: " + e.getMessage())); + src.sendFailure(Component.translatable(I + "error", e.getMessage())); e.printStackTrace(); } return 1; @@ -72,7 +75,7 @@ public static void register(RegisterCommandsEvent event) { Path projectDir = resolve(name, ctx.getSource().getServer()); if (Files.exists(projectDir)) { ctx.getSource().sendFailure( - Component.literal("Project already exists: " + name)); + Component.translatable(I + "create.exists", name)); return 0; } try { @@ -85,11 +88,11 @@ public static void register(RegisterCommandsEvent event) { + "console.log('" + name + " loaded');\n"; Files.writeString(projectDir.resolve("app.js"), template); ctx.getSource().sendSuccess( - () -> Component.literal("Project created: " + name - + "\nUse /box3script on " + name + " to enable it."), + () -> Component.translatable(I + "create.success", name, name), false); } catch (IOException e) { - ctx.getSource().sendFailure(Component.literal("Failed to create: " + e.getMessage())); + ctx.getSource().sendFailure( + Component.translatable(I + "create.error", e.getMessage())); } return 1; }))) @@ -98,7 +101,7 @@ public static void register(RegisterCommandsEvent event) { .executes(ctx -> { Box3ScriptEngine.get().reset(); ctx.getSource().sendSuccess( - () -> Component.literal("All scripts stopped. Callbacks cleared, scope reset."), + () -> Component.translatable(I + "stop"), false); return 1; })) @@ -109,18 +112,23 @@ public static void register(RegisterCommandsEvent event) { var config = Box3ScriptConfig.get(); config.discover(server); var projects = config.listProjects(); + var lang = Language.getInstance(); if (projects.isEmpty()) { ctx.getSource().sendSuccess( - () -> Component.literal("No projects found in config/box3/script/"), + () -> Component.translatable(I + "list.empty"), false); } else { - StringBuilder sb = new StringBuilder("Projects:\n"); + String labelOn = lang.getOrDefault(I + "list.on", "ON"); + String labelOff = lang.getOrDefault(I + "list.off", "OFF"); + StringBuilder sb = new StringBuilder( + lang.getOrDefault(I + "list.header", "Projects:") + "\n"); projects.forEach((name, enabled) -> { - sb.append(" ").append(enabled ? "§a[ON]" : "§c[OFF]") - .append(" ").append(name).append("\n"); + String status = enabled ? "§a[" + labelOn + "]" : "§c[" + labelOff + "]"; + sb.append(" ").append(status).append(" ").append(name).append("\n"); }); + String output = sb.toString().trim(); ctx.getSource().sendSuccess( - () -> Component.literal(sb.toString().trim()), + () -> Component.literal(output), false); } return 1; @@ -132,7 +140,15 @@ public static void register(RegisterCommandsEvent event) { String project = StringArgumentType.getString(ctx, "project"); Box3ScriptConfig.get().setEnabled(project, true); ctx.getSource().sendSuccess( - () -> Component.literal("Enabled: " + project), + () -> Component.translatable(I + "on.single", project), + false); + return 1; + })) + .then(literal("all") + .executes(ctx -> { + Box3ScriptConfig.get().setAllEnabled(true); + ctx.getSource().sendSuccess( + () -> Component.translatable(I + "on.all"), false); return 1; }))) @@ -143,7 +159,15 @@ public static void register(RegisterCommandsEvent event) { String project = StringArgumentType.getString(ctx, "project"); Box3ScriptConfig.get().setEnabled(project, false); ctx.getSource().sendSuccess( - () -> Component.literal("Disabled: " + project), + () -> Component.translatable(I + "off.single", project), + false); + return 1; + })) + .then(literal("all") + .executes(ctx -> { + Box3ScriptConfig.get().setAllEnabled(false); + ctx.getSource().sendSuccess( + () -> Component.translatable(I + "off.all"), false); return 1; }))) @@ -154,7 +178,7 @@ public static void register(RegisterCommandsEvent event) { Box3ScriptEngine.get().reset(); Box3ScriptEngine.get().autoLoad(server); ctx.getSource().sendSuccess( - () -> Component.literal("Scripts reloaded."), + () -> Component.translatable(I + "reload"), false); return 1; })) @@ -167,18 +191,18 @@ public static void register(RegisterCommandsEvent event) { var server = src.getServer(); Path appJs = resolve(project, server).resolve("app.js"); if (!Files.exists(appJs)) { - src.sendFailure(Component.literal("app.js not found: " + appJs)); + src.sendFailure(Component.translatable(I + "run.not_found", appJs)); return 0; } try { Box3ScriptEngine.get().init(server); Box3ScriptEngine.get().eval(Files.readString(appJs)); src.sendSuccess( - () -> Component.literal("Executed: " + project + "/app.js"), false); + () -> Component.translatable(I + "run.executed", project), false); } catch (IOException e) { - src.sendFailure(Component.literal("Failed to read: " + e.getMessage())); + src.sendFailure(Component.translatable(I + "run.read_error", e.getMessage())); } catch (Exception e) { - src.sendFailure(Component.literal("Script error: " + e.getMessage())); + src.sendFailure(Component.translatable(I + "error", e.getMessage())); e.printStackTrace(); } return 1; 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 index 7e7a1bfe..b7109b82 100644 --- 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 @@ -58,6 +58,11 @@ public void setEnabled(String projectName, boolean enabled) { save(); } + public void setAllEnabled(boolean enabled) { + projects.replaceAll((k, v) -> enabled); + save(); + } + public Map listProjects() { return new LinkedHashMap<>(projects); } diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/en_us.json b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/en_us.json index 0cb448fd..09f1ea17 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/en_us.json +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/en_us.json @@ -1,13 +1,23 @@ { - "itemGroup.box3js": "Example Mod Tab", - "block.box3js.example_block": "Example Block", - "item.box3js.example_item": "Example Item", - - "box3js.configuration.title": "Box3JS Configs", - "box3js.configuration.section.box3js.common.toml": "Box3JS Configs", - "box3js.configuration.section.box3js.common.toml.title": "Box3JS Configs", - "box3js.configuration.items": "Item List", - "box3js.configuration.logDirtBlock": "Log Dirt Block", - "box3js.configuration.magicNumberIntroduction": "Magic Number Text", - "box3js.configuration.magicNumber": "Magic Number" + "box3js.command.eval.success": "Script executed.", + "box3js.command.error": "Script error: %s", + "box3js.command.file.not_found": "File not found: %s", + "box3js.command.file.executed": "Executed: %s", + "box3js.command.file.read_error": "Failed to read file: %s", + "box3js.command.create.exists": "Project already exists: %s", + "box3js.command.create.success": "Project created: %s\nUse /box3script on %s to enable it.", + "box3js.command.create.error": "Failed to create: %s", + "box3js.command.stop": "All scripts stopped. Callbacks cleared, scope reset.", + "box3js.command.list.empty": "No projects found in config/box3/script/", + "box3js.command.list.header": "Projects:", + "box3js.command.list.on": "ON", + "box3js.command.list.off": "OFF", + "box3js.command.on.single": "Enabled: %s", + "box3js.command.on.all": "All projects enabled.", + "box3js.command.off.single": "Disabled: %s", + "box3js.command.off.all": "All projects disabled.", + "box3js.command.reload": "Scripts reloaded.", + "box3js.command.run.not_found": "app.js not found: %s", + "box3js.command.run.executed": "Executed: %s/app.js", + "box3js.command.run.read_error": "Failed to read: %s" } diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/zh_cn.json b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/zh_cn.json new file mode 100644 index 00000000..46ca071a --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/zh_cn.json @@ -0,0 +1,23 @@ +{ + "box3js.command.eval.success": "脚本已执行。", + "box3js.command.error": "脚本错误: %s", + "box3js.command.file.not_found": "文件未找到: %s", + "box3js.command.file.executed": "已执行: %s", + "box3js.command.file.read_error": "文件读取失败: %s", + "box3js.command.create.exists": "项目已存在: %s", + "box3js.command.create.success": "项目已创建: %s\n使用 /box3script on %s 来启用。", + "box3js.command.create.error": "创建失败: %s", + "box3js.command.stop": "所有脚本已停止。回调已清除,作用域已重置。", + "box3js.command.list.empty": "在 config/box3/script/ 中未找到项目", + "box3js.command.list.header": "项目列表:", + "box3js.command.list.on": "开", + "box3js.command.list.off": "关", + "box3js.command.on.single": "已启用: %s", + "box3js.command.on.all": "所有项目已启用。", + "box3js.command.off.single": "已禁用: %s", + "box3js.command.off.all": "所有项目已禁用。", + "box3js.command.reload": "脚本已重载。", + "box3js.command.run.not_found": "app.js 未找到: %s", + "box3js.command.run.executed": "已执行: %s/app.js", + "box3js.command.run.read_error": "读取失败: %s" +} From ca9bb731b6223771cae1b15c5e852b2d73c7dc85 Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Thu, 30 Apr 2026 15:56:56 +0800 Subject: [PATCH 05/17] =?UTF-8?q?feat(voxels,entity,player,world):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20GameVector3=20=E5=8F=82=E6=95=B0=E9=87=8D?= =?UTF-8?q?=E8=BD=BD=E6=96=B9=E6=B3=95=E5=8F=8A=E5=A4=9A=E9=A1=B9=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加接受 GameVector3 参数的 lookAt 方法重载(实体/玩家) - 添加接受 GameVector3 参数的 navigateTo 方法重载(实体) - 添加 flying 属性访问器(玩家飞行状态控制) - 添加 collision 属性访问器(玩家碰撞行为控制) - 添加 setPlayerListName 方法(自定义玩家 Tab 列表显示名称) - 添加 addExperienceLevels 方法(增加玩家经验等级) - 为方块操作添加接受 GameVector3 参数的重载方法(setVoxel/fillVoxel/countVoxel/setVoxelId/getVoxel/getVoxelId/getVoxelName/setSpawner/getVoxelRotation) - 为世界操作添加接受 GameVector3 参数的重载方法(strikeLightning/launchProjectile/launchFirework/spawnParticle/spawnParticleCircle/dropItem/entitiesInRadius/getBiome/explode/playSound) - 改进命令执行中的项目检测,为 require() 支持自动检测项目名称并使用 require('./app') 替代直接文件评估 docs: 移除过时的 BOX3_API_MAPPING 文档 BREAKING CHANGE: BOX3 API 映射文档已被移除,因其内容已过时 --- Box3JS-NeoForge-1.21.1/README.md | 141 ++++ .../docs/BOX3_API_MAPPING.md | 429 ---------- Box3JS-NeoForge-1.21.1/docs/api/README.md | 92 +++ Box3JS-NeoForge-1.21.1/docs/api/commands.md | 145 ++++ Box3JS-NeoForge-1.21.1/docs/api/entity.md | 419 ++++++++++ Box3JS-NeoForge-1.21.1/docs/api/math.md | 191 +++++ Box3JS-NeoForge-1.21.1/docs/api/player.md | 454 +++++++++++ Box3JS-NeoForge-1.21.1/docs/api/storage.md | 175 +++++ Box3JS-NeoForge-1.21.1/docs/api/voxels.md | 214 +++++ Box3JS-NeoForge-1.21.1/docs/api/world.md | 740 ++++++++++++++++++ .../box3lab/box3js/script/Box3JSEntity.java | 7 +- .../box3lab/box3js/script/Box3JSPlayer.java | 40 + .../box3lab/box3js/script/Box3JSVoxels.java | 39 + .../box3lab/box3js/script/Box3JSWorld.java | 46 +- .../box3js/script/Box3ScriptCommand.java | 12 +- .../box3js/script/Box3ScriptEngine.java | 40 +- 16 files changed, 2749 insertions(+), 435 deletions(-) create mode 100644 Box3JS-NeoForge-1.21.1/README.md delete mode 100644 Box3JS-NeoForge-1.21.1/docs/BOX3_API_MAPPING.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/api/README.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/api/commands.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/api/entity.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/api/math.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/api/player.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/api/storage.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/api/voxels.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/api/world.md diff --git a/Box3JS-NeoForge-1.21.1/README.md b/Box3JS-NeoForge-1.21.1/README.md new file mode 100644 index 00000000..1df7fa74 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/README.md @@ -0,0 +1,141 @@ +# Box3JS + +**Box3JS** 是一个 Minecraft NeoForge 1.21.1 模组,将 JavaScript 脚本引擎(Mozilla Rhino)嵌入到服务端中。允许用 JavaScript 编写游戏脚本——小游戏、机制扩展、自动化管理——而无需编写 Java 代码。 + +--- + +## 特性 + +- **JavaScript 运行时** — 使用 Rhino 1.7.14 引擎,编写 ES5/部分 ES6 代码 +- **Box3 API 兼容** — 实现了 Box3 平台核心 API(`world`、`entity`、`player`、`voxels`、`storage`) +- **MC 扩展 API** — 90+ Minecraft 独有功能:记分板、Bossbar、队伍、世界边界、粒子、烟花、闪电、药水效果、装备、属性、存储等 +- **热重载** — 使用 `/box3script reload` 重新加载脚本,无需重启服务端 +- **项目管理** — 多项目隔离,每个项目独立启用/禁用,下次启动自动执行 +- **多语言** — 所有命令提示支持英文和中文 + +--- + +## 安装 + +1. 下载 `.jar` 文件放入服务端 `mods/` 目录 +2. 启动服务端 +3. 脚本目录自动创建在 `config/box3/script/` + +**需求:** NeoForge 1.21.1+,Java 21+ + +--- + +## 快速开始 + +### 创建第一个脚本 + +在游戏中(需要 OP 权限): + +``` +/box3script create hello +/box3script on hello +/box3script run hello +``` + +这会创建 `config/box3/script/hello/app.js`: + +```js +// hello — Box3JS 项目 +world.onTick(() => { + // 每 tick 执行 +}); + +world.onChat((entity, message) => { + var p = entity.player; + if (message === "!hello") { + p.directMessage("你好," + p.name + "!"); + } +}); + +console.log("hello 已加载"); +``` + +### 目录结构 + +``` +config/box3/ + ├── scripts.json ← 项目启用/禁用配置 + ├── script/ + │ ├── hello/ + │ │ └── app.js + │ └── mygame/ + │ └── app.js + └── data/ ← storage API 数据文件 +``` + +--- + +## 可用 API + +| 对象 | 说明 | 文档 | +|---|---|---| +| `world` | 世界状态、事件、记分板、Bossbar、队伍、粒子、烟花 | [world.md](docs/api/world.md) | +| `entity` | 实体属性、AI、装备、药水、标签 | [entity.md](docs/api/entity.md) | +| `player` | 玩家背包、消息、飞行、游戏模式、传送 | [player.md](docs/api/player.md) | +| `voxels` | 方块读写、区域填充 | [voxels.md](docs/api/voxels.md) | +| `storage` | JSON 数据持久化 | [storage.md](docs/api/storage.md) | +| `GameVector3` 等 | 向量、包围盒、颜色、四元数 | [math.md](docs/api/math.md) | + +[API 总览 →](docs/api/README.md) + +--- + +## 命令 + +| 命令 | 说明 | +|---|---| +| `/box3script eval ` | 直接执行 JS 代码 | +| `/box3script file ` | 加载 JS 文件 | +| `/box3script create ` | 创建新项目 | +| `/box3script run ` | 运行一次项目 | +| `/box3script list` | 列出所有项目及状态 | +| `/box3script on ` | 启用项目 | +| `/box3script on all` | 启用所有 | +| `/box3script off ` | 禁用项目 | +| `/box3script off all` | 禁用所有 | +| `/box3script reload` | 重载所有已启用脚本 | +| `/box3script stop` | 停止所有脚本 | + +[命令详细参考 →](docs/api/commands.md) + +--- + +## 事件 + +脚本通过事件回调响应游戏行为: + +```js +world.onTick(() => { ... }); +world.onPlayerJoin((entity) => { ... }); +world.onPlayerLeave((entity) => { ... }); +world.onChat((entity, message, tick) => { ... }); +world.onEntityDeath((entity, killer, tick) => { ... }); +world.onEntityDamage((entity, amount, source, attacker, tick) => { ... }); +world.onPlayerRespawn((entity) => { ... }); +world.onVoxelDestroy((entity, x, y, z, voxel, tick) => { ... }); +world.onBlockPlace((entity, x, y, z, voxel, voxelId, tick) => { ... }); +world.onBlockActivate((entity, x, y, z, voxel, tick) => { ... }); +// 完整 17 种事件见 docs/api/world.md +``` + +--- + +## 构建 + +```bash +cd Box3JS-NeoForge-1.21.1 +./gradlew build +``` + +输出 JAR 在 `build/libs/box3js-.jar`。 + +--- + +## 许可证 + +Apache License 2.0 diff --git a/Box3JS-NeoForge-1.21.1/docs/BOX3_API_MAPPING.md b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_MAPPING.md deleted file mode 100644 index acbb454c..00000000 --- a/Box3JS-NeoForge-1.21.1/docs/BOX3_API_MAPPING.md +++ /dev/null @@ -1,429 +0,0 @@ -# Box3 API → MC 映射文档 - -## 概览 - -本文档记录了 Box3 平台 JS API 与 Minecraft (NeoForge 1.21.1) 之间的映射关系和实现状态。 - -- **✅ Box3 API** — Box3 原有 API,已接入 MC -- **⬆ MC 扩展** — 非 Box3 原有,利用 MC 特性新增 - -### 运行时 - -| 项目 | 说明 | -|---|---| -| 引擎 | Mozilla Rhino 1.7.14 | -| 作用域 | 服务端脚本(S- 脚本) | -| Tick | `ServerTickEvent.Post` | - -### console (⬆ MC 扩展) - -| API | 说明 | -|---|---| -| `console.log(...args)` | 标准日志输出 `[Box3JS] [项目名] msg` | -| `console.debug(...args)` | 调试日志 `[Box3JS][DEBUG] [项目名] msg` | -| `console.warn(...args)` | 警告日志 `[Box3JS][WARN] [项目名] msg` | -| `console.error(...args)` | 错误日志(stderr)`[Box3JS][ERROR] [项目名] msg` | -| `console.assert(assertion, ...args)` | 断言失败时调用 `error()` | -| `console.clear()` | 清空控制台 | - -### sleep (⬆ MC 扩展) - -| API | 说明 | -|---|---| -| `sleep(ms)` | 阻塞当前线程指定毫秒数 | - ---- - -## world (GameWorld) - -### 世界属性 - -| API | 类型 | MC 映射 | 说明 | -|---|---|---|---| -| `world.projectName()` | ✅ | `MinecraftServer.getMotd()` | | -| `world.currentTick()` | ✅ | `MinecraftServer.getTickCount()` | | -| `world.rainDensity` | ✅ | `getRainLevel()` / `setRaining()` | get/set,0.0–1.0 | -| `world.thunderDensity` | ⬆ | `getThunderLevel()` / `setThundering()` | get/set,0.0–1.0 | -| `world.clearWeather()` | ⬆ | `setRaining(false)` + `setThundering(false)` | 清除所有天气 | - -### 时间 - -| API | 类型 | MC 映射 | 说明 | -|---|---|---|---| -| `world.getTime()` | ✅ | `ServerLevel.getDayTime()` | | -| `world.setTime(tick)` | ✅ | `ServerLevel.setDayTime(tick)` | | -| `world.timeScale` | ✅ | `GameRules.RULE_DAYLIGHT` | get/set,0=停止 1=正常 | - -### 难度 / 出生点 - -| API | 类型 | MC 映射 | 说明 | -|---|---|---|---| -| `world.difficulty` | ✅ | `getDifficulty()` / `setDifficulty()` | get 返回名称字符串;set 接受字符串或 0-3 | -| `world.spawnPoint` | ⬆ | `ServerLevel.getSharedSpawnPos()` | 只读 GameVector3 | -| `world.setWorldSpawn(pos)` | ⬆ | `ServerLevel.setDefaultSpawnPos()` | | - -### 游戏规则 - -| API | 类型 | MC 映射 | 说明 | -|---|---|---|---| -| `world.getGameRule(name)` | ⬆ | `GameRules.getBoolean()` / `getInt()` | 支持规则名:doDaylightCycle, doWeatherCycle, keepInventory, doMobSpawning, doFireTick, mobGriefing, doImmediateRespawn | -| `world.setGameRule(name, value)` | ⬆ | `GameRule.set()` | value 为布尔值或数字字符串 | - -### 实体生成 - -| API | 类型 | MC 映射 | 说明 | -|---|---|---|---| -| `world.spawnEntity(type, pos)` | ✅ | `EntityType.create()` + `addFreshEntity()` | type 为命名空间 ID 字符串,返回 Box3JSEntity | - -### 事件 - -| API | 类型 | 回调参数 | -|---|---|---| -| `world.onTick(handler)` | ✅ | `()` | -| `world.onPlayerJoin(handler)` | ✅ | `(entity)` | -| `world.onPlayerLeave(handler)` | ✅ | `(entity)` | -| `world.onVoxelDestroy(handler)` | ✅ | `(entity, x, y, z, voxel, tick)` | -| `world.onVoxelContact(handler)` | ✅ | `(entity, voxel, x, y, z, axis, force, tick)` | -| `world.onInteract(handler)` | ✅ | `(entity, target, tick)` | -| `world.onChat(handler)` | ✅ | `(entity, message, tick)` | -| `world.onFluidEnter(handler)` | ✅ | `(entity, fluid, x, y, z, tick)` | -| `world.onFluidLeave(handler)` | ✅ | `(entity, fluid, x, y, z, tick)` | -| `world.onEntityContact(handler)` | ✅ | `(entity, other, tick)` | -| `world.onEntitySeparate(handler)` | ✅ | `(entity, other, tick)` | -| `world.onBlockPlace(handler)` | ⬆ | `(entity, x, y, z, voxel, voxelId, tick)` | -| `world.onEntityDeath(handler)` | ⬆ | `(entity, killer, tick)` | -| `world.onPlayerRespawn(handler)` | ⬆ | `(entity)` | -| `world.onBlockActivate(handler)` | ⬆ | `(entity, x, y, z, voxel, tick)` | -| `world.onEntityDamage(handler)` | ⬆ | `(entity, amount, source, attacker, tick)` | -| `world.onMessage(handler)` | ⬆ | `(from, data)` — 接收跨脚本消息 | - -### 查询 / 聊天 - -| API | 类型 | 说明 | -|---|---|---| -| `world.querySelector(selector)` | ✅ | `"*"` `"#uuid"` `".tag"` | -| `world.querySelectorAll(selector)` | ✅ | 同上,返回数组 | -| `world.say(message)` | ✅ | 全服广播 | - -### 计时器(⬆ MC 扩展) - -| API | 类型 | 说明 | -|---|---|---| -| `world.setTimeout(handler, ticks)` | ⬆ | 延迟执行,返回 timer ID | -| `world.setInterval(handler, ticks)` | ⬆ | 重复执行,返回 timer ID | -| `world.clearTimeout(id)` | ⬆ | 取消 timeout | -| `world.clearInterval(id)` | ⬆ | 取消 interval | - -### 记分板(⬆ MC 扩展) - -| API | 说明 | -|---|---| -| `world.addScoreboard(name)` | 创建 dummy 记分项 | -| `world.addScoreboard(name, criteria)` | 创建指定标准记分项 | -| `world.removeScoreboard(name)` | 删除记分项 | -| `world.setScore(entityOrName, obj, value)` | 设置分数 | -| `world.getScore(entityOrName, obj)` | 获取分数 | -| `world.showScoreboard(slot, obj)` | 显示记分板(slot: sidebar/list/belowname) | -| `world.hideScoreboard(slot)` | 清除显示槽位 | -| `world.listScores(obj)` | 获取所有分数条目 `[{name, value}]` | - -### Boss 血条(⬆ MC 扩展) - -| API | 说明 | -|---|---| -| `world.showBossbar(name, text, progress, color)` | 显示/更新 Boss 血条 | -| `world.removeBossbar(name)` | 移除 Boss 血条 | - -### 队伍(⬆ MC 扩展) - -| API | 说明 | -|---|---| -| `world.createTeam(name, color)` | 创建队伍 | -| `world.removeTeam(name)` | 删除队伍 | -| `world.joinTeam(entity, teamName)` | 加入队伍 | -| `world.leaveTeam(entity)` | 移出队伍 | -| `world.getTeamOf(entity)` | 获取队伍名称 | - -### 世界边界(⬆ MC 扩展) - -| API | 说明 | -|---|---| -| `world.getBorderSize()` | 获取边界大小 | -| `world.setBorderCenter(x, z)` | 设置边界中心 | -| `world.setBorderSize(size)` | 立即设置边界大小 | -| `world.shrinkBorder(target, sec)` | 平滑缩圈 | -| `world.setBorderDamage(d)` | 边界外伤害 | -| `world.setBorderWarning(blocks)` | 警告距离 | - -### 闪电 / 烟花 / 粒子 / 掉落物(⬆ MC 扩展) - -| API | 说明 | -|---|---| -| `world.strikeLightning(x, y, z)` | 召唤闪电 | -| `world.strikeLightning(x, y, z, damage)` | 召唤闪电(自定义伤害) | -| `world.launchFirework(x, y, z, color, shape)` | 发射烟花 | -| `world.spawnParticle(type, x, y, z, count, dx, dy, dz, speed)` | 生成粒子 | -| `world.spawnParticleCircle(x, y, z, radius, type, count)` | 圆形粒子圈 | -| `world.dropItem(x, y, z, itemId, count)` | 掉落物品 | -| `world.launchProjectile(type, x, y, z, tx, ty, tz, speed)` | 发射抛射物(火球、箭等) | - -### 爆炸 / 音效(⬆ MC 扩展) - -| API | 说明 | -|---|---| -| `world.explode(x, y, z, power)` | 创建爆炸 | -| `world.explode(x, y, z, power, fire)` | 创建爆炸(可引火) | -| `world.playSound(path, x, y, z, vol, pitch)` | 在坐标播放音效给所有玩家 | - -### 射线 / 查询(⬆ MC 扩展) - -| API | 说明 | -|---|---| -| `world.raycast(origin, dir)` | 射线检测,默认 5 格,返回 `{hit, x, y, z, normalX, normalY, normalZ, distance, entity, voxel}` | -| `world.raycast(origin, dir, maxDist)` | 射线检测,自定义距离 | -| `world.entitiesInArea(pos1, pos2)` | 返回 AABB 区域内所有实体 | -| `world.getBiome(x, y, z)` | 获取生物群系命名空间 ID | - -### 消息 / 命令(⬆ MC 扩展) - -| API | 说明 | -|---|---| -| `world.sendMessage(target, data)` | 精确路由到目标项目;`"*"` 广播给所有其他项目 | -| `world.runCommand(cmd)` | 以服务器身份执行命令 | - ---- - -## entity (GameEntity) - -| API | 类型 | 说明 | -|---|---|---| -| `.id` | ✅ | UUID 字符串,只读 | -| `.isPlayer()` | ✅ | | -| `.entityType` | ✅ | 命名空间 ID,只读 | -| `.position` | ✅ | LiveVec3;`.set(x,y,z)` 传送 | -| `.velocity` | ✅ | LiveVec3;`.set(x,y,z)` 修改 | -| `.bounds` | ✅ | 包围盒半尺寸,只读 | -| `.meshInvisible` | ✅ | 同步 `Entity.setInvisible()` | -| `.addTag(tag)` | ✅ | | -| `.hasTag(tag)` | ✅ | | -| `.removeTag(tag)` | ✅ | | -| `.hp` | ✅ | LivingEntity 同步 | -| `.maxHp` | ✅ | LivingEntity 同步 | -| `.destroyed` | ✅ | `Entity.isRemoved()`,只读 | -| `.hurt(amount)` | ✅ | | -| `.heal(amount)` | ✅ | | -| `.destroy()` | ✅ | 触发 onDestroy → discard | -| `.remove()` | ⬆ | `discard()` 不触发 onDestroy | -| `.onDestroy(handler)` | ✅ | | -| `.setFire(ticks)` | ⬆ | 点燃实体 | -| `.clearFire()` | ⬆ | 扑灭火焰 | -| `.lookAt(x, y, z)` | ⬆ | 实体面朝指定坐标 | -| `.navigateTo(x, y, z, speed)` | ⬆ | 寻路步行到目标(PathfinderMob) | -| `.setAI(enabled)` | ⬆ | 开关实体 AI | -| `.addEffect(id, dur, amp)` | ⬆ | 添加药水效果 | -| `.addEffect(id, dur, amp, hideParticles)` | ⬆ | 添加药水效果(可隐藏粒子) | -| `.setEquipment(slot, itemId)` | ⬆ | 给生物穿装备;slot: mainhand/offhand/head/chest/legs/feet | -| `.setTarget(entity)` | ⬆ | 设置怪物攻击目标(Mob.setTarget) | -| `.getTarget()` | ⬆ | 获取怪物当前攻击目标 | -| `.clearTarget()` | ⬆ | 清除攻击目标 | -| `.setDropChance(slot, chance)` | ⬆ | 设置装备掉落率 0-1;slot 可为 "all" | -| `.getAttribute(id)` | ⬆ | 获取实体属性值,如 `minecraft:generic.attack_damage` | -| `.setAttribute(id, value)` | ⬆ | 设置实体属性基值 | -| `.setPersistent(v)` | ⬆ | 设为 true 时生物不会自然消失 | -| `.isGlowing()` / `.setGlowing(v)` | ⬆ | 发光效果 | -| `.getNameTag()` / `.setNameTag(n)` | ⬆ | 自定义名称 | -| `.getOnGround()` | ⬆ | 是否在地面 | -| `.getEyePosition()` | ⬆ | 视线高度 GameVector3 | -| `.isInvulnerable()` / `.setInvulnerable(v)` | ⬆ | 无敌状态 | -| `entity.任意字段` | ✅ | 自定义属性,实体生命周期内有效 | - ---- - -## player (GamePlayerEntity) - -### 基本信息 / 外观 - -| API | 类型 | 说明 | -|---|---|---| -| `.name` | ✅ | 只读 | -| `.userId` | ✅ | UUID,只读 | -| `.invisible` | ✅ | get/set | -| `.scale` | ✅ | 只读 | - -### 移动 - -| API | 类型 | 说明 | -|---|---|---| -| `.walkSpeed` | ✅ | `MOVEMENT_SPEED` attribute | -| `.runSpeed` | ✅ | walkSpeed × 1.3 | -| `.jumpPower` | ✅ | `JUMP_STRENGTH` attribute | -| `.moveState` | ✅ | FLYING/SWIM/JUMP/FALL/GROUND | -| `.walkState` | ✅ | CROUCH/RUN/WALK/NONE | - -### 飞行 / 游戏模式 - -| API | 类型 | 说明 | -|---|---|---| -| `.canFly` | ✅ | `PlayerAbilities.mayfly` | -| `.spectator` | ✅ | 只读 | -| `.flySpeed` | ✅ | `PlayerAbilities.flyingSpeed` | -| `.disableFly` | ✅ | set true 时立即禁用飞行 | -| `.gameMode` | ✅ | get 返回名称;set 接受字符串或 0-3 | - -### 相机 - -| API | 类型 | 说明 | -|---|---|---| -| `.cameraMode` | ✅ | FPS 调用 `setCamera(null)` | -| `.cameraEntity` | ✅ | FOLLOW 调用 `setCamera(entity)` | -| `.cameraPitch` | ✅ | `getXRot()` / `setXRot()` | -| `.cameraYaw` | ✅ | `getYRot()` / `setYRot()` | -| `.facingDirection` | ✅ | 只读,`getLookAngle()` | -| `.cameraTarget` | ✅ | 只读,eye + look × 5.0 | - -### 重生 - -| API | 类型 | 说明 | -|---|---|---| -| `.setRespawnPoint(pos)` | ✅ | `player.setRespawnPosition()` | -| `.respawn()` | ✅ | `player.respawn()`(仅死亡时有效) | - -### 踢出 / 传送 - -| API | 类型 | 说明 | -|---|---|---| -| `.kick()` | ✅ | 默认 "Kicked" | -| `.kick(reason)` | ✅ | | -| `.teleport(pos)` | ✅ | | - -### 消息 - -| API | 类型 | 说明 | -|---|---|---| -| `.directMessage(msg)` | ✅ | | -| `.actionBar(msg)` | ✅ | 快捷栏上方 | -| `.title(title, subtitle)` | ⬆ | 默认 fadeIn=10 stay=70 fadeOut=20 | -| `.title(t, s, fIn, stay, fOut)` | ⬆ | 完全参数 | -| `.dialog(config)` | ✅ | 简化版,返回 `{index, value}` | -| `.link(href)` | ✅ | 可点击链接 | -| `.onChat(handler)` | ✅ | 玩家级聊天回调 | - -### 物品(⬆ MC 扩展) - -| API | 说明 | -|---|---| -| `.giveItem(itemId, count)` | 给予物品,命名空间 ID | -| `.giveEnchantedItem(itemId, count, enchants)` | 给予附魔物品;enchants 为 `{enchant_id: level}` 对象 | -| `.giveNamedItem(itemId, count, name, lore)` | 给予带名称/描述的物品;lore 为字符串数组 | -| `.getHeldItem()` | 主手物品,返回 `{id, count}` | -| `.clearInventory()` | 清空背包 | - -### 效果 / 属性(⬆ MC 扩展) - -| API | 说明 | -|---|---| -| `.addEffect(effectId, dur, amp)` | 添加药水效果,命名空间 ID;duration 为 tick | -| `.addEffect(effectId, dur, amp, hideParticles)` | 添加药水效果(可隐藏粒子) | -| `.clearEffects()` | 移除所有效果 | -| `.xp` | 经验等级 get/set | -| `.food` | 饱食度 get/set | -| `.saturation` | 饱和度 get/set | - -### 音效 / 维度(⬆ MC 扩展) - -| API | 说明 | -|---|---| -| `.playSound(path, vol, pitch)` | 播放任意 MC 音效给该玩家 | -| `.dimension` | 维度 ID,get/set(set 可跨维度传送) | -| `.lookAt(x, y, z)` | 玩家面朝指定坐标 | -| `.runCommand(cmd)` | 以玩家身份执行命令 | - ---- - -## 数学类型(全部 ✅) - -- **GameVector3** — `new(x,y,z)`、`.set` `.add` `.sub` `.scale` `.dot` `.mag` `.sqrMag` `.normalize` `.distance` `.lerp` `.equals`、`fromPolar()` -- **GameBounds3** — `new(lo,hi)`、`.intersects` `.contains` -- **GameRGBColor** — `new(r,g,b)` (0.0–1.0)、`.lerp`、`.random()` -- **GameRGBAColor** — `new(r,g,b,a)`、`.set` `.copy` `.clone` `.add/sub/mul/div` `.addEq/subEq/mulEq/divEq` `.lerp` `.equals` `.blendEq` -- **GameQuaternion** — `new(w,x,y,z)`、`.set` `.copy` `.clone` `.add/sub/mul/div` `.inv` `.dot` `.mag` `.sqrMag` `.normalize` `.slerp` `.angle` `.getAxisAngle` `.equals` `.rotateX/Y/Z`、`.fromAxisAngle` `.fromEuler` `.rotationBetween` - ---- - -## 枚举常量(全部 ✅) - -`GameDialogType` `GameButtonType` `GameInputDirection` `GameCameraMode` `GamePlayerMoveState` `GamePlayerWalkState` - ---- - -## voxels (GameVoxels) - -| API | 说明 | -|---|---| -| `voxels.shape` | 只读 | -| `voxels.VoxelTypes` | 方块名称数组 | -| `voxels.id(name)` / `voxels.name(id)` | 名称 ↔ ID | -| `voxels.setVoxel(x,y,z, voxel, rotation?)` | 放置方块;rotation 0-3 | -| `voxels.setVoxelId(x,y,z, voxel)` | voxel 含 rotation 编码 | -| `voxels.getVoxel(x,y,z)` | 基础 ID | -| `voxels.getVoxelId(x,y,z)` | 完整 ID | -| `voxels.getVoxelRotation(x,y,z)` | 0-3 | -| `voxels.fillVoxel(x1,y1,z1, x2,y2,z2, voxel)` | ⬆ 填充矩形区域 | -| `voxels.countVoxel(x1,y1,z1, x2,y2,z2, voxel)` | ⬆ 统计区域内匹配方块数量 | -| `voxels.setSpawner(x, y, z, entityType)` | ⬆ 设置刷怪笼刷出类型 | - ---- - -## storage (GameDataStorage) - -| API | 说明 | -|---|---| -| `storage.key` | 空字符串 | -| `storage.getDataStorage(name)` / `getGroupStorage(name)` | 返回 GameDataStorage | -| `store.set(key, value)` / `store.get(key)` | 读写 JSON | -| `store.keys()` | 返回所有 key | -| `store.update(key, handler)` | 回调更新 | -| `store.remove(key)` / `store.increment(key, delta?)` | 删除/递加 | -| `store.list(options)` | 分页排序过滤 | -| `store.destroy()` | 删除存储 | - ---- - -## 命令 - -| 命令 | 说明 | -|---|---| -| `/box3script eval ` | 执行 JS | -| `/box3script file ` | 加载执行 JS 文件 | -| `/box3script run ` | 运行一次项目的 app.js | -| `/box3script list` | 列出所有项目及开关状态 | -| `/box3script on ` | 启用项目 | -| `/box3script off ` | 禁用项目 | -| `/box3script reload` | 重载所有启用项目 | -| `/box3script stop` | 停止所有脚本,清空回调 | -| `/box3script create ` | 创建新项目目录及 `app.js` 模板 | - ---- - -## 永不能实现的 API - -| API | 原因 | -|---|---| -| `world.animate/getAnimations` | 世界关键帧动画 | -| `world.createEntity/createPlayerEntity` | 动态实体创建(用 `spawnEntity` 替代) | -| `.animation` `.setMotionControl` | Voxa 动作系统 | -| `.enable3DCursor` | 3D 光标 | -| `.boxId` `.userKey` `.querySocial` | Box3 平台账户 | -| `rtc` `analytics` `remoteChannel` `http` `defineParser` | 跨端通讯/语音/分析 | -| 全部客户端 API | 服务端无客户端上下文 | - ---- - -## 统计 - -| 状态 | 数量 | -|---|---| -| ✅ Box3 API | ~100 | -| ⬆ MC 扩展 | ~92 | - -> 最后更新:2026-04-30 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..5bbaa7b1 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/README.md @@ -0,0 +1,92 @@ +# Box3JS API 参考 + +Box3JS 是一个 Minecraft NeoForge 1.21.1 模组,允许用 JavaScript (Rhino 引擎) 编写服务端脚本。所有脚本运行在 `config/box3/script/<项目名>/app.js`。 + +## 快速开始 + +```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 | 实体包装,见 [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` 命令参考 | + +## 多文件模块 + +使用 CommonJS 的 `require()` / `module.exports` 来组织和导入多文件项目。每个文件是一个独立模块,通过 `require("./模块名")` 导入(自动追加 `.js` 后缀): + +``` +config/box3/script/skyrun/ +├── app.js ← 入口,require() 其他模块 +├── state.js ← 共享游戏状态 +├── course.js ← 赛道数据与建筑 +├── game.js ← 游戏流程控制 +├── checkpoints.js ← 检查点检测 +└── leaderboard.js ← 排行榜 +``` + +```js +// state.js — 导出共享状态 +var G = { phase: "lobby", checkpoints: [], ... }; +module.exports = { G: G, SB: "skyrun_scores" }; + +// app.js — 导入模块 +var G = require("./state").G; +var buildCourse = require("./course").buildCourse; +var startRace = require("./game").startRace; +``` + +> 注意:`require()` 使用 Rhino 内置的 CommonJS 模块系统,模块会被缓存供后续导入。仅在 `/box3script run ` 和自动加载时可用(需要项目上下文)。 + +## Tick 与性能 + +- 服务端每秒 20 tick,`world.onTick()` 每 tick 触发 +- `world.setInterval(handler, ticks)` 可以降低频率,例如 `setInterval(fn, 20)` = 每秒 1 次 +- 避免在 tick 中执行大量方块操作或实体遍历 +- 使用 `world.setTimeout(handler, ticks)` 做延时操作 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..1e705997 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/commands.md @@ -0,0 +1,145 @@ +# /box3script 命令参考 + +所有命令需要 **OP 权限等级 2**(默认管理员权限)。 + +--- + +## 命令列表 + +### `/box3script eval ` + +直接执行一段 JS 代码。 + +``` +/box3script eval world.say("hello") +/box3script eval var p = world.querySelectorAll("*")[0].player; p.teleport(new GameVector3(0,100,0)) +``` + +### `/box3script file ` + +加载并执行服务器上的 JS 文件。支持相对路径(相对于 `config/box3/script/`)和绝对路径。 + +``` +/box3script file my_script.js +/box3script file /home/server/scripts/test.js +``` + +### `/box3script run ` + +运行一次指定项目的 `app.js`(不改变启用状态)。 + +``` +/box3script run skyrun +``` + +### `/box3script create ` + +创建新的脚本项目。在 `config/box3/script//` 下创建目录和 `app.js` 模板文件。创建后默认**禁用**。 + +``` +/box3script create mygame +``` + +生成的文件结构: +``` +config/box3/script/ + └── mygame/ + └── app.js ← 模板脚本 +``` + +### `/box3script list` + +列出所有已发现的脚本项目及其启用/禁用状态。 + +``` +/box3script list +``` + +输出示例: +``` +项目列表: + [开] skyrun + [关] siege + [关] mygame +``` + +### `/box3script on ` + +启用指定项目。下次服务端重启时自动执行该项目的 `app.js`。 + +``` +/box3script on skyrun +``` + +### `/box3script on all` + +一键启用所有项目。 + +``` +/box3script on all +``` + +### `/box3script off ` + +禁用指定项目。下次服务端重启时不再自动执行。 + +``` +/box3script off siege +``` + +### `/box3script off all` + +一键禁用所有项目。 + +``` +/box3script off all +``` + +### `/box3script reload` + +停止所有脚本,重新加载所有已启用项目的 `app.js`。等价于 `stop` + 重新 `autoLoad`。 + +``` +/box3script reload +``` + +### `/box3script stop` + +立即停止所有正在运行的脚本。清除所有回调、定时器和作用域。 + +``` +/box3script stop +``` + +--- + +## 配置文件 + +启用/禁用状态保存在 `config/box3/scripts.json`: + +```json +{ + "skyrun": true, + "siege": false, + "mygame": true +} +``` + +--- + +## 脚本目录结构 + +``` +config/box3/ + ├── scripts.json ← 项目开关配置 + ├── script/ ← 脚本目录 + │ ├── skyrun/ + │ │ └── app.js ← 天空跑酷 + │ ├── siege/ + │ │ └── app.js ← 围攻游戏 + │ └── mygame/ + │ └── app.js ← 自定义项目 + └── data/ ← 存储数据目录 (storage API) + ├── skyrun_times/ + └── ... +``` 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..05391fea --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/entity.md @@ -0,0 +1,419 @@ +# 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.isInvulnerable() + +⬆ MC 扩展 | 实体是否无敌。 + +### entity.setInvulnerable(v) + +⬆ MC 扩展 | 设置实体无敌状态。 + +```js +entity.setInvulnerable(true); // 不受伤害 +``` + +--- + +## 外观 + +### entity.meshInvisible + +✅ Box3 API | 控制实体是否不可见。 + +```js +entity.meshInvisible = true; // 隐身 +``` + +### entity.isGlowing() + +⬆ MC 扩展 | 获取发光状态。 + +### entity.setGlowing(v) + +⬆ MC 扩展 | 设置发光效果(类似光灵箭效果)。 + +```js +entity.setGlowing(true); // 实体发光 +``` + +### 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); +``` + +--- + +## Box3 API 列表 + +| API | 类型 | +|---|---| +| `id` | ✅ Box3 | +| `isPlayer()` | ✅ Box3 | +| `entityType` | ✅ Box3 | +| `position` | ✅ Box3 | +| `velocity` | ✅ Box3 | +| `bounds` | ✅ Box3 | +| `meshInvisible` | ✅ Box3 | +| `addTag()` / `hasTag()` / `removeTag()` | ✅ Box3 | +| `hp` / `maxHp` | ✅ Box3 | +| `hurt()` / `heal()` | ✅ Box3 | +| `destroy()` / `destroyed` | ✅ Box3 | +| `setOnDestroy()` | ✅ Box3 | + +## MC 扩展列表 + +| API | 类型 | +|---|---| +| `onGround` | ⬆ MC | +| `eyePosition` | ⬆ MC | +| `isInvulnerable()` / `setInvulnerable()` | ⬆ MC | +| `isGlowing()` / `setGlowing()` | ⬆ MC | +| `nameTag` | ⬆ MC | +| `setFire()` / `clearFire()` | ⬆ MC | +| `setAI()` | ⬆ MC | +| `setTarget()` / `getTarget()` / `clearTarget()` | ⬆ MC | +| `navigateTo()` | ⬆ MC | +| `lookAt()` | ⬆ MC | +| `addEffect()` | ⬆ MC | +| `setEquipment()` | ⬆ MC | +| `setDropChance()` | ⬆ MC | +| `getAttribute()` / `setAttribute()` | ⬆ MC | +| `remove()` | ⬆ MC | +| `setPersistent()` | ⬆ MC | 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..55b1ec7e --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/math.md @@ -0,0 +1,191 @@ +# 数学类型 + +全部 ✅ 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(pitch, yaw); // 极坐标 → 向量 +``` + +```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 copy = a.copy(); // 浅拷贝 +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()` | 旋转角度 | +| `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(yaw, pitch, roll); +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..028127a2 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/player.md @@ -0,0 +1,454 @@ +# 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.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 +``` + +--- + +## 游戏模式 + +### 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.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); +``` + +--- + +## Box3 API 列表 + +| API | 类型 | +|---|---| +| `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 参) | ✅ Box3 | +| `link()` | ✅ Box3 | +| `onChat()` (player-level) | ✅ Box3 | + +## MC 扩展列表 + +| API | 类型 | +|---|---| +| `collision` | ⬆ MC | +| `title()` (5 参) | ⬆ MC | +| `xp` / `addExperienceLevels()` | ⬆ MC | +| `food` / `saturation` | ⬆ MC | +| `giveItem()` / `clearInventory()` / `getHeldItem()` | ⬆ MC | +| `giveEnchantedItem()` / `giveNamedItem()` | ⬆ MC | +| `addEffect()` (3/4 参) / `clearEffects()` | ⬆ MC | +| `playSound()` | ⬆ MC | +| `dimension` | ⬆ MC | +| `lookAt()` | ⬆ MC | +| `runCommand()` | ⬆ MC | +| `setPlayerListName()` | ⬆ 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..63073ea0 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/storage.md @@ -0,0 +1,175 @@ +# storage — 数据存储 API + +`storage` 提供 JSON 文件持久化存储,项目间数据隔离。数据保存在 `config/box3/data/<项目名>/` 目录下。 + +--- + +## 获取存储实例 + +### storage.getDataStorage(name) + +✅ Box3 API | 获取或创建一个命名存储。同名存储返回同一实例。 + +### storage.getGroupStorage(name) + +✅ Box3 API | 获取组存储(目前与 `getDataStorage` 行为一致)。 + +```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` 对象支持的字段: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `limit` | number | 返回最多条目数 | +| `offset` | number | 跳过的条目数 | +| `sort` | string | 排序方式,`"asc"` 或 `"desc"`(按 key 排序) | +| `filter` | string | 过滤条件(前缀匹配) | + +返回 `[{key, value}]` 数组。 + +```js +// 返回前 10 条 +var top10 = store.list({ limit: 10, sort: "desc" }); + +// 第 11–20 条 +var page2 = store.list({ limit: 10, offset: 10 }); + +// 查找以 "player_" 开头的 key +var playerData = store.list({ filter: "player_" }); + +for (var i = 0; i < top10.length; i++) { + console.log(top10[i].key + ": " + top10[i].value); +} +``` + +--- + +## 完整示例:排行榜 + +```js +var lb = storage.getDataStorage("leaderboard"); + +// 保存新成绩 +function saveScore(name, time) { + var entry = JSON.stringify({ + name: name, + time: time, + date: new Date().toISOString() + }); + lb.set("entry_" + Date.now(), entry); +} + +// 获取排行榜 +function getLeaderboard() { + var entries = lb.list({ limit: 10, sort: "asc" }); + var result = []; + for (var i = 0; i < entries.length; i++) { + result.push(JSON.parse(entries[i].value)); + } + result.sort(function(a, b) { return a.time - b.time; }); + return result; +} + +saveScore("Steve", 12345); +saveScore("Alex", 9800); + +var top = getLeaderboard(); +for (var i = 0; i < top.length; i++) { + console.log((i + 1) + ". " + top[i].name + " - " + top[i].time); +} +``` + +--- + +全部 ✅ 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..0806d04e --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/voxels.md @@ -0,0 +1,214 @@ +# 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"); +``` + +--- + +## 常用方块 ID 参考 + +```js +// 建筑方块 +"minecraft:stone" "minecraft:stone_bricks" "minecraft:cobblestone" +"minecraft:glass" "minecraft:white_concrete" "minecraft:oak_planks" +"minecraft:obsidian" "minecraft:bedrock" "minecraft:gold_block" +"minecraft:diamond_block" "minecraft:beacon" + +// 装饰 +"minecraft:sea_lantern" "minecraft:glowstone" +"minecraft:white_stained_glass" "minecraft:cyan_terracotta" + +// 特殊 +"minecraft:air" // 清除方块 +"minecraft:spawner" // 刷怪笼 +"minecraft:water" // 水 +"minecraft:lava" // 岩浆 +``` + +--- + +## Box3 API 列表 + +| API | 类型 | +|---|---| +| `shape` | ✅ Box3 | +| `VoxelTypes` | ✅ Box3 | +| `id()` / `name()` | ✅ Box3 | +| `setVoxel()` | ✅ Box3 | +| `setVoxelId()` | ✅ Box3 | +| `getVoxel()` / `getVoxelId()` / `getVoxelName()` | ✅ Box3 | +| `getVoxelRotation()` | ✅ Box3 | + +## MC 扩展列表 + +| API | 类型 | +|---|---| +| `fillVoxel()` | ⬆ MC | +| `countVoxel()` | ⬆ MC | +| `setSpawner()` | ⬆ MC | 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..d7fd52c4 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/world.md @@ -0,0 +1,740 @@ +# 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.getTime() + +✅ Box3 API | 返回当前世界时间(tick)。 + +### world.setTime(tick) + +✅ Box3 API | 设置世界时间(tick)。Minecraft 一天 = 24000 tick。 + +```js +world.setTime(6000); // 正午 +world.setTime(18000); // 午夜 +``` + +常用时间值:`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, voxel, x, y, z, axis, 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.getBorderSize() + +返回当前边界直径。 + +### world.setBorderCenter(x, z) + +设置边界中心。 + +### world.setBorderSize(size) + +立即设置边界大小。 + +### world.shrinkBorder(targetSize, seconds) + +边界平滑缩小到目标大小,耗时 `seconds` 秒。 + +### world.setBorderDamage(damagePerBlock) + +边界外每秒伤害值。 + +### world.setBorderWarning(blocks) + +边界警告距离(屏幕变红的提前量)。 + +```js +// 缩圈玩法 +world.setBorderCenter(0, 0); +world.setBorderSize(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"); +``` + +--- + +## Box3 API 列表 + +| API | 类型 | +|---|---| +| `currentTick()` | ✅ Box3 | +| `rainDensity` | ✅ Box3 | +| `getTime()` / `setTime()` | ✅ Box3 | +| `timeScale` | ✅ Box3 | +| `difficulty` | ✅ Box3 | +| `spawnEntity()` | ✅ Box3 | +| `onTick()` | ✅ Box3 | +| `onPlayerJoin()` | ✅ Box3 | +| `onPlayerLeave()` | ✅ Box3 | +| `onVoxelDestroy()` | ✅ Box3 | +| `onVoxelContact()` | ✅ Box3 | +| `onInteract()` | ✅ Box3 | +| `onChat()` | ✅ Box3 | +| `onFluidEnter()` | ✅ Box3 | +| `onFluidLeave()` | ✅ Box3 | +| `onEntityContact()` | ✅ Box3 | +| `onEntitySeparate()` | ✅ Box3 | +| `querySelectorAll()` | ✅ Box3 | +| `querySelector()` | ✅ Box3 | +| `say()` | ✅ Box3 | + +## MC 扩展列表 + +| API | 类型 | +|---|---| +| `thunderDensity` | ⬆ MC | +| `clearWeather()` | ⬆ MC | +| `spawnPoint` / `setWorldSpawn()` | ⬆ MC | +| `getGameRule()` / `setGameRule()` | ⬆ MC | +| `onBlockPlace()` | ⬆ MC | +| `onEntityDeath()` | ⬆ MC | +| `onPlayerRespawn()` | ⬆ MC | +| `onBlockActivate()` | ⬆ MC | +| `onEntityDamage()` | ⬆ MC | +| `onMessage()` | ⬆ MC | +| `setTimeout()` / `setInterval()` / `clearTimeout()` / `clearInterval()` | ⬆ MC | +| `addScoreboard()` / `removeScoreboard()` / `setScore()` / `getScore()` | ⬆ MC | +| `showScoreboard()` / `hideScoreboard()` / `listScores()` | ⬆ MC | +| `showBossbar()` / `removeBossbar()` | ⬆ MC | +| `createTeam()` / `removeTeam()` / `joinTeam()` / `leaveTeam()` / `getTeamOf()` | ⬆ MC | +| `getBorderSize()` / `setBorderCenter()` / `setBorderSize()` / `shrinkBorder()` | ⬆ MC | +| `setBorderDamage()` / `setBorderWarning()` | ⬆ MC | +| `strikeLightning()` | ⬆ MC | +| `launchFirework()` | ⬆ MC | +| `spawnParticle()` / `spawnParticleCircle()` | ⬆ MC | +| `dropItem()` | ⬆ MC | +| `launchProjectile()` | ⬆ MC | +| `explode()` | ⬆ MC | +| `playSound()` | ⬆ MC | +| `raycast()` | ⬆ MC | +| `entitiesInArea()` / `entitiesInRadius()` | ⬆ MC | +| `getBiome()` | ⬆ MC | +| `sendMessage()` / `runCommand()` | ⬆ MC | 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 index 3063d056..ac784ce9 100644 --- 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 @@ -198,16 +198,21 @@ public void lookAt(double x, double y, double z) { entity.setYRot(yaw); entity.setXRot(pitch); } + public void lookAt(GameVector3 pos) { + lookAt(pos.x, pos.y, pos.z); + } // ---- Navigation (MC extension) ---- - /** Pathfinding-based movement to a target position. Returns true if a path was found. */ 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) { 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 index 3db64b5a..adf2c4dd 100644 --- 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 @@ -90,6 +90,25 @@ public void setCanFly(boolean v) { player.onUpdateAbilities(); } + public boolean getFlying() { return player.getAbilities().flying; } + public void setFlying(boolean v) { + player.getAbilities().flying = v; + player.onUpdateAbilities(); + } + + 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) { + 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(); } @@ -260,6 +279,20 @@ public void link(String href) { 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) { @@ -270,6 +303,9 @@ public void lookAt(double x, double y, double z) { player.setYRot((float) (Math.toDegrees(Math.atan2(dz, dx)) - 90.0)); player.setXRot((float) (-Math.toDegrees(Math.atan2(dy, hd)))); } + public void lookAt(GameVector3 pos) { + lookAt(pos.x, pos.y, pos.z); + } // ---- Command ---- @@ -283,6 +319,10 @@ public void runCommand(String cmd) { 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); } 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 index 6eda0fc6..b29e9f13 100644 --- 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 @@ -107,6 +107,10 @@ public String name(int id) { 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) { @@ -131,6 +135,10 @@ public int setVoxel(int x, int y, int z, Object voxel, Object rotation) { 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) { @@ -145,6 +153,10 @@ public void fillVoxel(int x1, int y1, int z1, int x2, int y2, int z2, Object vox } } } + /** 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) { @@ -167,6 +179,10 @@ public int countVoxel(int x1, int y1, int z1, int x2, int y2, int z2, Object vox } 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) { @@ -188,6 +204,10 @@ public int setVoxelId(int x, int y, int z, int voxel) { 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 ---- @@ -199,6 +219,10 @@ public int getVoxel(int x, int y, int z) { 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) { @@ -211,6 +235,10 @@ public int getVoxelId(int x, int y, int z) { 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) { @@ -224,6 +252,10 @@ public String getVoxelName(int x, int y, int z) { } 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) { @@ -239,6 +271,9 @@ public void setSpawner(int x, int y, int z, String entityType) { spawnerBe.setEntityId(opt.get(), 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) { @@ -246,6 +281,10 @@ public int getVoxelRotation(int x, int y, int z) { 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) { 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 index de5d0769..706a16bf 100644 --- 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 @@ -493,6 +493,9 @@ public boolean strikeLightning(double x, double y, double z) { level.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) { ServerLevel level = server.overworld(); LightningBolt bolt = EntityType.LIGHTNING_BOLT.create(level); @@ -503,6 +506,9 @@ public boolean strikeLightning(double x, double y, double z, double damage) { level.addFreshEntity(bolt); return true; } + public boolean strikeLightning(GameVector3 pos, double damage) { + return strikeLightning(pos.x, pos.y, pos.z, damage); + } // ---- Projectile (MC extension) ---- @@ -530,6 +536,9 @@ public Box3JSEntity launchProjectile(String type, double x, double y, double z, level.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 ---- @@ -564,6 +573,9 @@ public void launchFirework(double x, double y, double z, String color, String sh var entity = new net.minecraft.world.entity.projectile.FireworkRocketEntity(level, x, y, z, rocket); level.addFreshEntity(entity); } + public void launchFirework(GameVector3 pos, String color, String shape) { + launchFirework(pos.x, pos.y, pos.z, color, shape); + } // ---- Particle ---- @@ -573,6 +585,9 @@ public void spawnParticle(String type, double x, double y, double z, int count, 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 = resolveParticle(type); if (particle == null) return; @@ -584,6 +599,9 @@ public void spawnParticleCircle(double x, double y, double z, double radius, Str level.sendParticles(particle, px, y, pz, 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); + } private ParticleOptions resolveParticle(String type) { ResourceLocation rl = ResourceLocation.tryParse(type); if (rl == null) return null; @@ -606,6 +624,9 @@ public void dropItem(double x, double y, double z, String itemId, int count) { ItemEntity itemEntity = new ItemEntity(level, x, y, z, stack); level.addFreshEntity(itemEntity); } + public void dropItem(GameVector3 pos, String itemId, int count) { + dropItem(pos.x, pos.y, pos.z, itemId, count); + } // ---- Query ---- @@ -683,21 +704,41 @@ public List entitiesInArea(GameVector3 pos1, GameVector3 pos2) { return result; } + public 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, e -> true)) { + result.add(new Box3JSEntity(e, server, engine)); + } + return result; + } + public List entitiesInRadius(GameVector3 pos, double radius) { + return entitiesInRadius(pos.x, pos.y, pos.z, radius); + } + public 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"); } + public String getBiome(GameVector3 pos) { + return getBiome((int) pos.x, (int) pos.y, (int) pos.z); + } // ---- 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 ---- @@ -711,6 +752,9 @@ public void playSound(String path, double x, double y, double z, double volume, 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 ---- 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 index 3f67056b..065f7209 100644 --- 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 @@ -56,6 +56,13 @@ public static void register(RegisterCommandsEvent event) { } try { Box3ScriptEngine.get().init(server); + // Detect project name for require() support + Path scriptDir = server.getServerDirectory().resolve("config/box3/script"); + Path relative = null; + try { relative = scriptDir.relativize(filePath.toAbsolutePath()); } catch (Exception ignored) {} + if (relative != null && relative.getNameCount() > 1) { + Box3ScriptEngine.get().setCurrentProject(relative.getName(0).toString()); + } Box3ScriptEngine.get().eval(Files.readString(filePath)); src.sendSuccess( () -> Component.translatable(I + "file.executed", filePath.getFileName()), false); @@ -196,11 +203,10 @@ public static void register(RegisterCommandsEvent event) { } try { Box3ScriptEngine.get().init(server); - Box3ScriptEngine.get().eval(Files.readString(appJs)); + Box3ScriptEngine.get().setCurrentProject(project); + Box3ScriptEngine.get().eval("require('./app')"); src.sendSuccess( () -> Component.translatable(I + "run.executed", project), false); - } catch (IOException e) { - src.sendFailure(Component.translatable(I + "run.read_error", e.getMessage())); } catch (Exception e) { src.sendFailure(Component.translatable(I + "error", e.getMessage())); e.printStackTrace(); 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 index 33d66642..b7acec37 100644 --- 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 @@ -6,8 +6,14 @@ 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.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -48,6 +54,7 @@ public class Box3ScriptEngine { private final Set entityContactPairs = ConcurrentHashMap.newKeySet(); private final Map playerChatHandlers = new ConcurrentHashMap<>(); private final Map> entityCustomProps = new HashMap<>(); + private final Map projectRequires = new HashMap<>(); private final List timers = new ArrayList<>(); private int timerIdCounter; @@ -83,7 +90,7 @@ public void autoLoad(MinecraftServer server) { if (Files.exists(appJs) && config.isEnabled(name)) { try { setCurrentProject(name); - eval(Files.readString(appJs)); + eval("require('./app')"); Box3JS.LOGGER.info("Auto-loaded project: {}", name); } catch (Exception e) { Box3JS.LOGGER.error("Failed to auto-load: {}", appJs, e); @@ -433,6 +440,7 @@ public void reset() { entityCustomProps.clear(); timers.clear(); timerIdCounter = 0; + projectRequires.clear(); this.worldBinding = new Box3JSWorld(server, this); this.voxelsBinding = new Box3JSVoxels(server); this.storageBinding = new Box3JSStorage(server.getServerDirectory().resolve("config"), this); @@ -463,6 +471,36 @@ private void setupScope() { " }" + "};", "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.singletonList(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) { From 64c4fc643daf3b3eaea2eaa432010ce7ec3908fe Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Thu, 30 Apr 2026 18:02:17 +0800 Subject: [PATCH 06/17] =?UTF-8?q?feat(script):=20=E4=B8=BA=20app.js=20?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E6=B7=BB=E5=8A=A0=E5=9B=9E=E9=80=80=E6=9C=BA?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优先检查项目目录中的 dist/app.js 文件 - 若 dist/app.js 不存在,则回退到根目录的 app.js - 更新模块脚本提供者,同时搜索 dist 和根目录 此变更允许脚本引擎适配主 JavaScript 文件位于 dist 文件夹或项目根目录的项目。 --- .../java/com/box3lab/box3js/script/Box3ScriptEngine.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 index b7acec37..26e02999 100644 --- 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 @@ -86,7 +86,10 @@ public void autoLoad(MinecraftServer server) { .sorted() .forEach(project -> { String name = project.getFileName().toString(); - Path appJs = project.resolve("app.js"); + 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); @@ -484,7 +487,9 @@ public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] ar try { ModuleScriptProvider provider = new StrongCachingModuleScriptProvider( new UrlModuleSourceProvider( - Collections.singletonList(projectDir.toUri()), null) { + Collections.unmodifiableList(java.util.Arrays.asList( + projectDir.resolve("dist").toUri(), + projectDir.toUri())), null) { @Override protected String getCharacterEncoding(java.net.URLConnection c) { return "utf-8"; From 2274b129bd0c80742052e13debb1eaad98bbc4d4 Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Thu, 30 Apr 2026 19:25:09 +0800 Subject: [PATCH 07/17] =?UTF-8?q?docs(api):=20=E6=9B=B4=E6=96=B0=E5=AE=9E?= =?UTF-8?q?=E4=BD=93=E5=B1=9E=E6=80=A7=E6=96=B9=E6=B3=95=E5=B9=B6=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20getOpLevel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 isInvulnerable/setInvulnerable 替换为 invulnerable 属性 - 将 isGlowing/setGlowing 替换为 glowing 属性 - 为玩家 API 文档新增 getOpLevel 方法 - 更新示例代码以使用新的属性语法 - 相应更新文档目录 --- Box3JS-NeoForge-1.21.1/docs/api/entity.md | 26 +++---- Box3JS-NeoForge-1.21.1/docs/api/math.md | 6 +- Box3JS-NeoForge-1.21.1/docs/api/player.md | 11 +++ Box3JS-NeoForge-1.21.1/docs/api/storage.md | 72 +++++++++---------- Box3JS-NeoForge-1.21.1/docs/api/world.md | 45 ++++++------ .../box3lab/box3js/script/Box3JSPlayer.java | 2 + 6 files changed, 84 insertions(+), 78 deletions(-) diff --git a/Box3JS-NeoForge-1.21.1/docs/api/entity.md b/Box3JS-NeoForge-1.21.1/docs/api/entity.md index 05391fea..db4da692 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/entity.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/entity.md @@ -105,16 +105,13 @@ zombie.hurt(10); // 造成 10 点伤害 zombie.heal(5); // 治疗 5 点 ``` -### entity.isInvulnerable() +### entity.invulnerable -⬆ MC 扩展 | 实体是否无敌。 - -### entity.setInvulnerable(v) - -⬆ MC 扩展 | 设置实体无敌状态。 +⬆ MC 扩展 | 获取/设置实体是否无敌。 ```js -entity.setInvulnerable(true); // 不受伤害 +entity.invulnerable = true; // 不受伤害 +console.log(entity.invulnerable); ``` --- @@ -129,16 +126,13 @@ entity.setInvulnerable(true); // 不受伤害 entity.meshInvisible = true; // 隐身 ``` -### entity.isGlowing() - -⬆ MC 扩展 | 获取发光状态。 - -### entity.setGlowing(v) +### entity.glowing -⬆ MC 扩展 | 设置发光效果(类似光灵箭效果)。 +⬆ MC 扩展 | 获取/设置发光效果(类似光灵箭效果)。 ```js -entity.setGlowing(true); // 实体发光 +entity.glowing = true; // 实体发光 +console.log(entity.glowing); ``` ### entity.nameTag @@ -403,8 +397,8 @@ console.log(entity.myCustomField); |---|---| | `onGround` | ⬆ MC | | `eyePosition` | ⬆ MC | -| `isInvulnerable()` / `setInvulnerable()` | ⬆ MC | -| `isGlowing()` / `setGlowing()` | ⬆ MC | +| `invulnerable` | ⬆ MC | +| `glowing` | ⬆ MC | | `nameTag` | ⬆ MC | | `setFire()` / `clearFire()` | ⬆ MC | | `setAI()` | ⬆ MC | diff --git a/Box3JS-NeoForge-1.21.1/docs/api/math.md b/Box3JS-NeoForge-1.21.1/docs/api/math.md index 55b1ec7e..389fa7e1 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/math.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/math.md @@ -41,7 +41,7 @@ v.z = 30; ### 静态方法 ```js -var v = GameVector3.fromPolar(pitch, yaw); // 极坐标 → 向量 +var v = GameVector3.fromPolar(mag, phi, theta); // 球坐标 → 向量 ``` ```js @@ -177,7 +177,7 @@ var q = new GameQuaternion(0, 0, 0, 1); // w, x, y, z | `q.mag()` / `q.sqrMag()` | 模长 | | `q.normalize()` | 归一化 | | `q.slerp(p, t)` | 球面线性插值 | -| `q.angle()` | 旋转角度 | +| `q.angle(p)` | 与另一四元数的夹角 (弧度) | | `q.getAxisAngle()` | 获取旋转轴和角度 | | `q.rotateX(a)` / `q.rotateY(a)` / `q.rotateZ(a)` | 绕轴旋转 | | `q.equals(p)` | 比较 | @@ -186,6 +186,6 @@ var q = new GameQuaternion(0, 0, 0, 1); // w, x, y, z ```js var q1 = GameQuaternion.fromAxisAngle(axis, angle); -var q2 = GameQuaternion.fromEuler(yaw, pitch, roll); +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 index 028127a2..4ed4b61a 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/player.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/player.md @@ -21,6 +21,16 @@ world.onPlayerJoin((entity) => { ✅ Box3 API | 只读。玩家 UUID 字符串。 +### player.getOpLevel() + +⬆ MC 扩展 | 返回玩家管理员权限等级 (0-4)。0=普通玩家, 1=可绕过出生点保护, 2=可使用大部分命令, 3=可管理玩家, 4=最高权限。 + +```js +if (player.getOpLevel() >= 2) { + // 需要权限等级 2 的操作 +} +``` + --- ## 外观 @@ -452,3 +462,4 @@ player.setPlayerListName(player.name); | `lookAt()` | ⬆ MC | | `runCommand()` | ⬆ MC | | `setPlayerListName()` | ⬆ MC | +| `getOpLevel()` | ⬆ MC | diff --git a/Box3JS-NeoForge-1.21.1/docs/api/storage.md b/Box3JS-NeoForge-1.21.1/docs/api/storage.md index 63073ea0..29309252 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/storage.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/storage.md @@ -107,29 +107,39 @@ store.increment("kills", -2); // kills = 4 ### store.list(options) -✅ Box3 API | 分页排序查询。`options` 对象支持的字段: +✅ Box3 API | 游标分页查询。`options` 对象支持的字段: | 字段 | 类型 | 说明 | |---|---|---| -| `limit` | number | 返回最多条目数 | -| `offset` | number | 跳过的条目数 | -| `sort` | string | 排序方式,`"asc"` 或 `"desc"`(按 key 排序) | -| `filter` | string | 过滤条件(前缀匹配) | +| `cursor` | number | 起始游标(页码 × pageSize) | +| `pageSize` | number | 每页条目数(1–100,默认 100) | +| `ascending` | boolean | 是否升序排列 | +| `max` | number | 值的上限过滤 | +| `min` | number | 值的下限过滤 | +| `constraintTarget` | string | 排序/过滤的嵌套路径(如 `"a.b.c"`) | -返回 `[{key, value}]` 数组。 +返回 `QueryList` 分页对象: -```js -// 返回前 10 条 -var top10 = store.list({ limit: 10, sort: "desc" }); +| 属性/方法 | 说明 | +|---|---| +| `result.isLastPage` | 是否最后一页 | +| `result.getCurrentPage()` | 返回当前页条目数组 | +| `result.nextPage()` | 移到下一页 | + +每条条目为 `{key, value, updateTime, createTime, version}`。 -// 第 11–20 条 -var page2 = store.list({ limit: 10, offset: 10 }); +```js +var result = store.list({ pageSize: 10, ascending: false }); -// 查找以 "player_" 开头的 key -var playerData = store.list({ filter: "player_" }); +// 遍历当前页 +var page = result.getCurrentPage(); +for (var i = 0; i < page.length; i++) { + console.log(page[i].key + ": " + page[i].value); +} -for (var i = 0; i < top10.length; i++) { - console.log(top10[i].key + ": " + top10[i].value); +// 下一页 +if (!result.isLastPage) { + result.nextPage(); } ``` @@ -140,33 +150,23 @@ for (var i = 0; i < top10.length; i++) { ```js var lb = storage.getDataStorage("leaderboard"); -// 保存新成绩 +// 保存成绩 function saveScore(name, time) { - var entry = JSON.stringify({ - name: name, - time: time, - date: new Date().toISOString() - }); - lb.set("entry_" + Date.now(), entry); -} - -// 获取排行榜 -function getLeaderboard() { - var entries = lb.list({ limit: 10, sort: "asc" }); - var result = []; - for (var i = 0; i < entries.length; i++) { - result.push(JSON.parse(entries[i].value)); - } - result.sort(function(a, b) { return a.time - b.time; }); - return result; + lb.set(name, time); } saveScore("Steve", 12345); saveScore("Alex", 9800); -var top = getLeaderboard(); -for (var i = 0; i < top.length; i++) { - console.log((i + 1) + ". " + top[i].name + " - " + top[i].time); +// 遍历所有条目 +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(); } ``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/world.md b/Box3JS-NeoForge-1.21.1/docs/api/world.md index d7fd52c4..d3326d48 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/world.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/world.md @@ -6,20 +6,20 @@ ## 世界属性 -### world.projectName() +### world.projectName -⬆ MC 扩展 | 返回服务端 MOTD 字符串。 +⬆ MC 扩展 | 只读。服务端 MOTD 字符串。 ```js -console.log(world.projectName()); // "A Minecraft Server" +console.log(world.projectName); // "A Minecraft Server" ``` -### world.currentTick() +### world.currentTick -✅ Box3 API | 返回服务器自启动以来的总 tick 数。 +✅ Box3 API | 只读。服务器自启动以来的总 tick 数。 ```js -var uptime = world.currentTick(); +var uptime = world.currentTick; world.say("服务器已运行 " + Math.floor(uptime / 20 / 60) + " 分钟"); ``` @@ -56,17 +56,20 @@ world.clearWeather(); ## 时间 -### world.getTime() +### world.time -✅ Box3 API | 返回当前世界时间(tick)。 +✅ Box3 API | 获取/设置世界时间(tick)。Minecraft 一天 = 24000 tick。 -### world.setTime(tick) +```js +world.time = 6000; // 正午 +world.time = 18000; // 午夜 +console.log(world.time); // 当前时间 +``` -✅ Box3 API | 设置世界时间(tick)。Minecraft 一天 = 24000 tick。 +此外提供 `world.setTime(tick)` 方法作为便捷设置接口。 ```js -world.setTime(6000); // 正午 -world.setTime(18000); // 午夜 +world.setTime(6000); // 等效于 world.time = 6000 ``` 常用时间值:`0` 日出、`6000` 正午、`12000` 日落、`18000` 午夜。 @@ -175,7 +178,7 @@ zombie.setAI(true); | `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, voxel, x, y, z, axis, force, 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)` | 实体进入液体 | @@ -418,18 +421,14 @@ world.onPlayerJoin((entity) => { 全部 ⬆ MC 扩展。 -### world.getBorderSize() +### world.borderSize -返回当前边界直径。 +获取/设置当前边界大小。 ### world.setBorderCenter(x, z) 设置边界中心。 -### world.setBorderSize(size) - -立即设置边界大小。 - ### world.shrinkBorder(targetSize, seconds) 边界平滑缩小到目标大小,耗时 `seconds` 秒。 @@ -445,7 +444,7 @@ world.onPlayerJoin((entity) => { ```js // 缩圈玩法 world.setBorderCenter(0, 0); -world.setBorderSize(500); +world.borderSize = 500; world.setBorderDamage(2); world.setBorderWarning(10); @@ -685,9 +684,9 @@ world.runCommand("weather clear"); | API | 类型 | |---|---| -| `currentTick()` | ✅ Box3 | +| `currentTick` | ✅ Box3 | | `rainDensity` | ✅ Box3 | -| `getTime()` / `setTime()` | ✅ Box3 | +| `time` / `setTime()` | ✅ Box3 | | `timeScale` | ✅ Box3 | | `difficulty` | ✅ Box3 | | `spawnEntity()` | ✅ Box3 | @@ -725,7 +724,7 @@ world.runCommand("weather clear"); | `showScoreboard()` / `hideScoreboard()` / `listScores()` | ⬆ MC | | `showBossbar()` / `removeBossbar()` | ⬆ MC | | `createTeam()` / `removeTeam()` / `joinTeam()` / `leaveTeam()` / `getTeamOf()` | ⬆ MC | -| `getBorderSize()` / `setBorderCenter()` / `setBorderSize()` / `shrinkBorder()` | ⬆ MC | +| `borderSize` / `setBorderCenter()` / `shrinkBorder()` | ⬆ MC | | `setBorderDamage()` / `setBorderWarning()` | ⬆ MC | | `strikeLightning()` | ⬆ MC | | `launchFirework()` | ⬆ MC | 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 index adf2c4dd..8a487329 100644 --- 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 @@ -40,6 +40,8 @@ public Box3JSPlayer(ServerPlayer player, MinecraftServer server, Box3ScriptEngin public String getName() { return player.getGameProfile().getName(); } public String getUserId() { return player.getUUID().toString(); } + public int getOpLevel() { return server.getProfilePermissions(player.getGameProfile()); } + // ---- Appearance ---- public boolean getInvisible() { return player.isInvisible(); } From e9dcde52f992d4f9f13b544f7502874e484239ad Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Thu, 30 Apr 2026 19:50:55 +0800 Subject: [PATCH 08/17] =?UTF-8?q?feat(api):=20=E6=96=B0=E5=A2=9E=20player.?= =?UTF-8?q?dialog=20=E6=96=B9=E6=B3=95=E7=94=A8=E4=BA=8E=E6=A8=A1=E6=80=81?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 player.dialog() 方法,支持显示带有可配置内容和选项的模态对话框。 该方法接收包含 content 和 options 属性的配置对象,并返回选中的索引和值。 docs(api): 更新数学文档中 GameRGBAColor.copy 的用法 将示例从直接使用 a.copy() 改为先创建新的 GameRGBAColor 实例再调用 copy(), 使浅拷贝操作在文档中更清晰。 refactor(api): 将世界属性转换为方法调用 将 world.projectName 和 world.currentTick 从属性改为方法调用, 在文档示例和 API 参考表中均添加括号。 fix(cmd): 移除本地化系统并简化命令消息 将所有可翻译组件替换为字符串字面量,并移除语言文件资源。 此举简化了命令输出,移除了对本地化文件的依赖,同时保持相同功能。 --- Box3JS-NeoForge-1.21.1/docs/api/math.md | 3 +- Box3JS-NeoForge-1.21.1/docs/api/player.md | 13 +++++ Box3JS-NeoForge-1.21.1/docs/api/world.md | 10 ++-- .../box3js/script/Box3ScriptCommand.java | 51 ++++++++----------- .../resources/assets/box3js/lang/en_us.json | 23 --------- .../resources/assets/box3js/lang/zh_cn.json | 23 --------- 6 files changed, 42 insertions(+), 81 deletions(-) delete mode 100644 Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/en_us.json delete mode 100644 Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/zh_cn.json diff --git a/Box3JS-NeoForge-1.21.1/docs/api/math.md b/Box3JS-NeoForge-1.21.1/docs/api/math.md index 389fa7e1..0ea018d6 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/math.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/math.md @@ -145,7 +145,8 @@ a.divEq(b); // 原地除法 a.blendEq(b); // 混合 a.set(0.5, 0.5, 0.5, 1); // 设置分量 -var copy = a.copy(); // 浅拷贝 +var result = new GameRGBAColor(0, 0, 0, 0); +result.copy(a); // 浅拷贝 a var clone = a.clone(); // 深拷贝 var lerped = a.lerp(b, 0.5); // 插值 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/player.md b/Box3JS-NeoForge-1.21.1/docs/api/player.md index 4ed4b61a..5309b788 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/player.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/player.md @@ -253,6 +253,18 @@ player.kick("你已被移出游戏"); ⬆ 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 | 向玩家发送可点击链接。 @@ -443,6 +455,7 @@ player.setPlayerListName(player.name); | `teleport()` | ✅ Box3 | | `directMessage()` / `actionBar()` | ✅ Box3 | | `title()` (2 参) | ✅ Box3 | +| `dialog()` | ✅ Box3 | | `link()` | ✅ Box3 | | `onChat()` (player-level) | ✅ Box3 | diff --git a/Box3JS-NeoForge-1.21.1/docs/api/world.md b/Box3JS-NeoForge-1.21.1/docs/api/world.md index d3326d48..3b5a12eb 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/world.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/world.md @@ -6,20 +6,20 @@ ## 世界属性 -### world.projectName +### world.projectName() ⬆ MC 扩展 | 只读。服务端 MOTD 字符串。 ```js -console.log(world.projectName); // "A Minecraft Server" +console.log(world.projectName()); // "A Minecraft Server" ``` -### world.currentTick +### world.currentTick() ✅ Box3 API | 只读。服务器自启动以来的总 tick 数。 ```js -var uptime = world.currentTick; +var uptime = world.currentTick(); world.say("服务器已运行 " + Math.floor(uptime / 20 / 60) + " 分钟"); ``` @@ -684,7 +684,7 @@ world.runCommand("weather clear"); | API | 类型 | |---|---| -| `currentTick` | ✅ Box3 | +| `currentTick()` | ✅ Box3 | | `rainDensity` | ✅ Box3 | | `time` / `setTime()` | ✅ Box3 | | `timeScale` | ✅ Box3 | 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 index 065f7209..1b05cb08 100644 --- 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 @@ -2,7 +2,6 @@ import com.mojang.brigadier.arguments.StringArgumentType; import net.minecraft.commands.CommandSourceStack; -import net.minecraft.locale.Language; import net.minecraft.network.chat.Component; import net.minecraft.server.MinecraftServer; import net.neoforged.neoforge.event.RegisterCommandsEvent; @@ -16,8 +15,6 @@ public class Box3ScriptCommand { - private static final String I = "box3js.command."; - public static void register(RegisterCommandsEvent event) { var dispatcher = event.getDispatcher(); @@ -34,10 +31,10 @@ public static void register(RegisterCommandsEvent event) { Box3ScriptEngine.get().init(server); Box3ScriptEngine.get().eval(code); ctx.getSource().sendSuccess( - () -> Component.translatable(I + "eval.success"), false); + () -> Component.literal("Script executed."), false); } catch (Exception e) { ctx.getSource().sendFailure( - Component.translatable(I + "error", e.getMessage())); + Component.literal("Script error: " + e.getMessage())); e.printStackTrace(); } return 1; @@ -51,7 +48,7 @@ public static void register(RegisterCommandsEvent event) { var server = src.getServer(); Path filePath = resolve(input, server); if (!Files.exists(filePath)) { - src.sendFailure(Component.translatable(I + "file.not_found", filePath)); + src.sendFailure(Component.literal("File not found: " + filePath)); return 0; } try { @@ -65,11 +62,11 @@ public static void register(RegisterCommandsEvent event) { } Box3ScriptEngine.get().eval(Files.readString(filePath)); src.sendSuccess( - () -> Component.translatable(I + "file.executed", filePath.getFileName()), false); + () -> Component.literal("Executed: " + filePath.getFileName()), false); } catch (IOException e) { - src.sendFailure(Component.translatable(I + "file.read_error", e.getMessage())); + src.sendFailure(Component.literal("Failed to read file: " + e.getMessage())); } catch (Exception e) { - src.sendFailure(Component.translatable(I + "error", e.getMessage())); + src.sendFailure(Component.literal("Script error: " + e.getMessage())); e.printStackTrace(); } return 1; @@ -82,7 +79,7 @@ public static void register(RegisterCommandsEvent event) { Path projectDir = resolve(name, ctx.getSource().getServer()); if (Files.exists(projectDir)) { ctx.getSource().sendFailure( - Component.translatable(I + "create.exists", name)); + Component.literal("Project already exists: " + name)); return 0; } try { @@ -95,11 +92,11 @@ public static void register(RegisterCommandsEvent event) { + "console.log('" + name + " loaded');\n"; Files.writeString(projectDir.resolve("app.js"), template); ctx.getSource().sendSuccess( - () -> Component.translatable(I + "create.success", name, name), + () -> Component.literal("Project created: " + name + "\nUse /box3script on " + name + " to enable it."), false); } catch (IOException e) { ctx.getSource().sendFailure( - Component.translatable(I + "create.error", e.getMessage())); + Component.literal("Failed to create: " + e.getMessage())); } return 1; }))) @@ -108,7 +105,7 @@ public static void register(RegisterCommandsEvent event) { .executes(ctx -> { Box3ScriptEngine.get().reset(); ctx.getSource().sendSuccess( - () -> Component.translatable(I + "stop"), + () -> Component.literal("All scripts stopped. Callbacks cleared, scope reset."), false); return 1; })) @@ -119,19 +116,15 @@ public static void register(RegisterCommandsEvent event) { var config = Box3ScriptConfig.get(); config.discover(server); var projects = config.listProjects(); - var lang = Language.getInstance(); if (projects.isEmpty()) { ctx.getSource().sendSuccess( - () -> Component.translatable(I + "list.empty"), + () -> Component.literal("No projects found in config/box3/script/"), false); } else { - String labelOn = lang.getOrDefault(I + "list.on", "ON"); - String labelOff = lang.getOrDefault(I + "list.off", "OFF"); - StringBuilder sb = new StringBuilder( - lang.getOrDefault(I + "list.header", "Projects:") + "\n"); + StringBuilder sb = new StringBuilder("§6=== Projects ===\n"); projects.forEach((name, enabled) -> { - String status = enabled ? "§a[" + labelOn + "]" : "§c[" + labelOff + "]"; - sb.append(" ").append(status).append(" ").append(name).append("\n"); + String status = enabled ? "§a[ON]" : "§c[OFF]"; + sb.append(" ").append(status).append(" §f").append(name).append("\n"); }); String output = sb.toString().trim(); ctx.getSource().sendSuccess( @@ -147,7 +140,7 @@ public static void register(RegisterCommandsEvent event) { String project = StringArgumentType.getString(ctx, "project"); Box3ScriptConfig.get().setEnabled(project, true); ctx.getSource().sendSuccess( - () -> Component.translatable(I + "on.single", project), + () -> Component.literal("Enabled: " + project), false); return 1; })) @@ -155,7 +148,7 @@ public static void register(RegisterCommandsEvent event) { .executes(ctx -> { Box3ScriptConfig.get().setAllEnabled(true); ctx.getSource().sendSuccess( - () -> Component.translatable(I + "on.all"), + () -> Component.literal("All projects enabled."), false); return 1; }))) @@ -166,7 +159,7 @@ public static void register(RegisterCommandsEvent event) { String project = StringArgumentType.getString(ctx, "project"); Box3ScriptConfig.get().setEnabled(project, false); ctx.getSource().sendSuccess( - () -> Component.translatable(I + "off.single", project), + () -> Component.literal("Disabled: " + project), false); return 1; })) @@ -174,7 +167,7 @@ public static void register(RegisterCommandsEvent event) { .executes(ctx -> { Box3ScriptConfig.get().setAllEnabled(false); ctx.getSource().sendSuccess( - () -> Component.translatable(I + "off.all"), + () -> Component.literal("All projects disabled."), false); return 1; }))) @@ -185,7 +178,7 @@ public static void register(RegisterCommandsEvent event) { Box3ScriptEngine.get().reset(); Box3ScriptEngine.get().autoLoad(server); ctx.getSource().sendSuccess( - () -> Component.translatable(I + "reload"), + () -> Component.literal("Scripts reloaded."), false); return 1; })) @@ -198,7 +191,7 @@ public static void register(RegisterCommandsEvent event) { var server = src.getServer(); Path appJs = resolve(project, server).resolve("app.js"); if (!Files.exists(appJs)) { - src.sendFailure(Component.translatable(I + "run.not_found", appJs)); + src.sendFailure(Component.literal("app.js not found: " + appJs)); return 0; } try { @@ -206,9 +199,9 @@ public static void register(RegisterCommandsEvent event) { Box3ScriptEngine.get().setCurrentProject(project); Box3ScriptEngine.get().eval("require('./app')"); src.sendSuccess( - () -> Component.translatable(I + "run.executed", project), false); + () -> Component.literal("Executed: " + project + "/app.js"), false); } catch (Exception e) { - src.sendFailure(Component.translatable(I + "error", e.getMessage())); + src.sendFailure(Component.literal("Script error: " + e.getMessage())); e.printStackTrace(); } return 1; diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/en_us.json b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/en_us.json deleted file mode 100644 index 09f1ea17..00000000 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/en_us.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "box3js.command.eval.success": "Script executed.", - "box3js.command.error": "Script error: %s", - "box3js.command.file.not_found": "File not found: %s", - "box3js.command.file.executed": "Executed: %s", - "box3js.command.file.read_error": "Failed to read file: %s", - "box3js.command.create.exists": "Project already exists: %s", - "box3js.command.create.success": "Project created: %s\nUse /box3script on %s to enable it.", - "box3js.command.create.error": "Failed to create: %s", - "box3js.command.stop": "All scripts stopped. Callbacks cleared, scope reset.", - "box3js.command.list.empty": "No projects found in config/box3/script/", - "box3js.command.list.header": "Projects:", - "box3js.command.list.on": "ON", - "box3js.command.list.off": "OFF", - "box3js.command.on.single": "Enabled: %s", - "box3js.command.on.all": "All projects enabled.", - "box3js.command.off.single": "Disabled: %s", - "box3js.command.off.all": "All projects disabled.", - "box3js.command.reload": "Scripts reloaded.", - "box3js.command.run.not_found": "app.js not found: %s", - "box3js.command.run.executed": "Executed: %s/app.js", - "box3js.command.run.read_error": "Failed to read: %s" -} diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/zh_cn.json b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/zh_cn.json deleted file mode 100644 index 46ca071a..00000000 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/lang/zh_cn.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "box3js.command.eval.success": "脚本已执行。", - "box3js.command.error": "脚本错误: %s", - "box3js.command.file.not_found": "文件未找到: %s", - "box3js.command.file.executed": "已执行: %s", - "box3js.command.file.read_error": "文件读取失败: %s", - "box3js.command.create.exists": "项目已存在: %s", - "box3js.command.create.success": "项目已创建: %s\n使用 /box3script on %s 来启用。", - "box3js.command.create.error": "创建失败: %s", - "box3js.command.stop": "所有脚本已停止。回调已清除,作用域已重置。", - "box3js.command.list.empty": "在 config/box3/script/ 中未找到项目", - "box3js.command.list.header": "项目列表:", - "box3js.command.list.on": "开", - "box3js.command.list.off": "关", - "box3js.command.on.single": "已启用: %s", - "box3js.command.on.all": "所有项目已启用。", - "box3js.command.off.single": "已禁用: %s", - "box3js.command.off.all": "所有项目已禁用。", - "box3js.command.reload": "脚本已重载。", - "box3js.command.run.not_found": "app.js 未找到: %s", - "box3js.command.run.executed": "已执行: %s/app.js", - "box3js.command.run.read_error": "读取失败: %s" -} From 11990b84061e948c8d4655eb9b028effe3d7912a Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Thu, 30 Apr 2026 20:11:55 +0800 Subject: [PATCH 09/17] =?UTF-8?q?feat(readme):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=96=87=E6=A1=A3=EF=BC=8C=E5=A2=9E=E5=8A=A0=20TypeScript=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=92=8C=E6=94=B9=E8=BF=9B=E7=9A=84=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: 更新 README 以反映新的基于 TypeScript 的项目模板、 更新的 API 描述和修订的安装说明。该模组现在使用 Rhino 1.9.1 配合 Babel 转译为 ES5 以实现兼容性,而非直接支持 ES5/ES6。 - 在 README 顶部添加测试版警告提示 - 更新功能章节,包含 TypeScript 支持、CommonJS 模块和 esbuild 集成 - 修订安装步骤,反映 JAR 放置位置和依赖要求 - 重写快速入门指南,演示使用 npm install 和构建流程的 TypeScript 项目创建工作流 - 更新命令参考表,使用更简洁的描述 - 添加测试版已知限制章节 - 更新事件示例,使用函数声明替代箭头函数 - 修改构建输出路径描述 fix(command): 实现 TypeScript 项目模板复制功能 - 新增 TEMPLATE_FILES 数组,包含 TypeScript 项目模板路径 - 创建 copyTemplate 方法,从 classpath 复制 TypeScript 模板文件 - 更新项目创建成功消息,包含 npm 构建说明 - 添加 InputStream 导入以支持模板文件读取 - 将硬编码的 JavaScript 模板替换为正确的 TypeScript 模板文件, 包括 package.json、tsconfig.json、build.mjs 和类型声明文件 --- Box3JS-NeoForge-1.21.1/README.md | 125 +- .../box3js/script/Box3ScriptCommand.java | 51 +- .../assets/box3js/template/build.mjs | 34 + .../assets/box3js/template/gitignore.template | 3 + .../assets/box3js/template/package.json | 17 + .../assets/box3js/template/src/app.ts | 38 + .../assets/box3js/template/tsconfig.json | 14 + .../assets/box3js/template/types/globals.d.ts | 2220 +++++++++++++++++ 8 files changed, 2432 insertions(+), 70 deletions(-) create mode 100644 Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/build.mjs create mode 100644 Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/gitignore.template create mode 100644 Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/package.json create mode 100644 Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/app.ts create mode 100644 Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/tsconfig.json create mode 100644 Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/globals.d.ts diff --git a/Box3JS-NeoForge-1.21.1/README.md b/Box3JS-NeoForge-1.21.1/README.md index 1df7fa74..db2b9e60 100644 --- a/Box3JS-NeoForge-1.21.1/README.md +++ b/Box3JS-NeoForge-1.21.1/README.md @@ -1,71 +1,68 @@ # Box3JS -**Box3JS** 是一个 Minecraft NeoForge 1.21.1 模组,将 JavaScript 脚本引擎(Mozilla Rhino)嵌入到服务端中。允许用 JavaScript 编写游戏脚本——小游戏、机制扩展、自动化管理——而无需编写 Java 代码。 +> **测试版(Beta)** — 本项目处于早期测试阶段,API 可能变动,可能存在未发现的缺陷。欢迎反馈问题。 + +**Box3JS** 是一个 Minecraft NeoForge 1.21.1 服务端模组,将 JavaScript 运行时(Mozilla Rhino 1.9.1)嵌入服务端。无需编写 Java 代码,用 JS/TS 即可编写服务端脚本——小游戏、机制扩展、自动化管理。 --- ## 特性 -- **JavaScript 运行时** — 使用 Rhino 1.7.14 引擎,编写 ES5/部分 ES6 代码 -- **Box3 API 兼容** — 实现了 Box3 平台核心 API(`world`、`entity`、`player`、`voxels`、`storage`) -- **MC 扩展 API** — 90+ Minecraft 独有功能:记分板、Bossbar、队伍、世界边界、粒子、烟花、闪电、药水效果、装备、属性、存储等 -- **热重载** — 使用 `/box3script reload` 重新加载脚本,无需重启服务端 -- **项目管理** — 多项目隔离,每个项目独立启用/禁用,下次启动自动执行 -- **多语言** — 所有命令提示支持英文和中文 +- **JS 运行时** — Rhino 1.9.1 引擎,通过 Babel 向下兼容 ES5 +- **TypeScript 支持** — 项目模板内置 TS 类型声明,esbuild 打包,完整类型检查 +- **Box3 API 兼容** — 实现了 Box3 平台核心 API(World / Entity / Player / Voxels / Storage) +- **MC 扩展** — 90+ Minecraft 独有功能:记分板、Bossbar、队伍、世界边界、粒子、烟花、药水等 +- **CommonJS 模块** — `require()` 多文件组织,支持大型脚本项目 +- **热重载** — `/box3script reload` 重新加载,无需重启 +- **项目管理** — 多项目隔离,独立启用/禁用,重启自动执行 --- ## 安装 -1. 下载 `.jar` 文件放入服务端 `mods/` 目录 +1. 将 JAR 放入服务端 `mods/` 目录 2. 启动服务端 3. 脚本目录自动创建在 `config/box3/script/` -**需求:** NeoForge 1.21.1+,Java 21+ +**需求:** NeoForge 1.21.1,Java 21 --- ## 快速开始 -### 创建第一个脚本 +在游戏中(需要 OP 权限,等级 ≥ 2): + +``` +/box3script create mygame +``` -在游戏中(需要 OP 权限): +这会创建一个 TypeScript 脚手架项目: ``` -/box3script create hello -/box3script on hello -/box3script run hello +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 ← 入口 ``` -这会创建 `config/box3/script/hello/app.js`: +然后构建: -```js -// hello — Box3JS 项目 -world.onTick(() => { - // 每 tick 执行 -}); - -world.onChat((entity, message) => { - var p = entity.player; - if (message === "!hello") { - p.directMessage("你好," + p.name + "!"); - } -}); - -console.log("hello 已加载"); +```bash +cd config/box3/script/mygame +npm install +npm run build # 输出 dist/app.js ``` -### 目录结构 +回到游戏启用: ``` -config/box3/ - ├── scripts.json ← 项目启用/禁用配置 - ├── script/ - │ ├── hello/ - │ │ └── app.js - │ └── mygame/ - │ └── app.js - └── data/ ← storage API 数据文件 +/box3script on mygame +/box3script reload ``` --- @@ -79,7 +76,7 @@ config/box3/ | `player` | 玩家背包、消息、飞行、游戏模式、传送 | [player.md](docs/api/player.md) | | `voxels` | 方块读写、区域填充 | [voxels.md](docs/api/voxels.md) | | `storage` | JSON 数据持久化 | [storage.md](docs/api/storage.md) | -| `GameVector3` 等 | 向量、包围盒、颜色、四元数 | [math.md](docs/api/math.md) | +| 数学类型 | Vector3、Bounds3、Color、Quaternion | [math.md](docs/api/math.md) | [API 总览 →](docs/api/README.md) @@ -89,17 +86,15 @@ config/box3/ | 命令 | 说明 | |---|---| -| `/box3script eval ` | 直接执行 JS 代码 | -| `/box3script file ` | 加载 JS 文件 | -| `/box3script create ` | 创建新项目 | +| `/box3script create ` | 创建 TS 脚手架项目 | | `/box3script run ` | 运行一次项目 | -| `/box3script list` | 列出所有项目及状态 | -| `/box3script on ` | 启用项目 | -| `/box3script on all` | 启用所有 | -| `/box3script off ` | 禁用项目 | -| `/box3script off all` | 禁用所有 | +| `/box3script list` | 列出所有项目及启用状态 | +| `/box3script on ` | 启用项目 | +| `/box3script off ` | 禁用项目 | | `/box3script reload` | 重载所有已启用脚本 | | `/box3script stop` | 停止所有脚本 | +| `/box3script eval ` | 直接执行 JS 代码 | +| `/box3script file ` | 加载 JS 文件 | [命令详细参考 →](docs/api/commands.md) @@ -107,24 +102,32 @@ config/box3/ ## 事件 -脚本通过事件回调响应游戏行为: - ```js -world.onTick(() => { ... }); -world.onPlayerJoin((entity) => { ... }); -world.onPlayerLeave((entity) => { ... }); -world.onChat((entity, message, tick) => { ... }); -world.onEntityDeath((entity, killer, tick) => { ... }); -world.onEntityDamage((entity, amount, source, attacker, tick) => { ... }); -world.onPlayerRespawn((entity) => { ... }); -world.onVoxelDestroy((entity, x, y, z, voxel, tick) => { ... }); -world.onBlockPlace((entity, x, y, z, voxel, voxelId, tick) => { ... }); -world.onBlockActivate((entity, x, y, z, voxel, tick) => { ... }); -// 完整 17 种事件见 docs/api/world.md +world.onTick(function () { ... }); +world.onPlayerJoin(function (entity) { ... }); +world.onPlayerLeave(function (entity) { ... }); +world.onChat(function (entity, message, tick) { ... }); +world.onEntityDeath(function (entity, killer, tick) { ... }); +world.onEntityDamage(function (entity, amount, source, attacker, tick) { ... }); +world.onPlayerRespawn(function (entity) { ... }); +world.onVoxelDestroy(function (entity, x, y, z, voxel, tick) { ... }); +world.onBlockPlace(function (entity, x, y, z, voxel, voxelId, tick) { ... }); +world.onBlockActivate(function (entity, x, y, z, voxel, tick) { ... }); +// 共 17 种事件,完整列表见 docs/api/world.md ``` --- +## 已知限制(测试版) + +- 仅支持 NeoForge 1.21.1(Fabric / 其他 MC 版本暂未适配) +- Rhino 1.9.1 仅支持到 ES5 语法(class / 箭头函数 / 模板字符串由 Babel 转译) +- `player.dialog()` 为简化实现,仅发送系统消息 +- 部分 Box3 API(如 UI 相关)在服务端环境下不适用 +- 暂无自动化测试覆盖 + +--- + ## 构建 ```bash @@ -132,7 +135,7 @@ cd Box3JS-NeoForge-1.21.1 ./gradlew build ``` -输出 JAR 在 `build/libs/box3js-.jar`。 +输出:`build/libs/box3js-.jar` --- 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 index 1b05cb08..fa5e98c4 100644 --- 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 @@ -7,8 +7,10 @@ import net.neoforged.neoforge.event.RegisterCommandsEvent; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import static net.minecraft.commands.Commands.literal; import static net.minecraft.commands.Commands.argument; @@ -83,16 +85,12 @@ public static void register(RegisterCommandsEvent event) { return 0; } try { - Files.createDirectories(projectDir); - String template = "// " + name + " — Box3JS project\n" - + "world.onTick(() => {\n" - + " // 每 tick 执行\n" - + "});\n" - + "\n" - + "console.log('" + name + " loaded');\n"; - Files.writeString(projectDir.resolve("app.js"), template); + copyTemplate(projectDir, name); ctx.getSource().sendSuccess( - () -> Component.literal("Project created: " + name + "\nUse /box3script on " + name + " to enable it."), + () -> Component.literal("Project created: " + name + + "\n cd config/box3/script/" + name + + "\n npm install && npm run build" + + "\nUse /box3script on " + name + " to enable it."), false); } catch (IOException e) { ctx.getSource().sendFailure( @@ -209,6 +207,41 @@ public static void register(RegisterCommandsEvent event) { ); } + private static final String[] TEMPLATE_FILES = { + "gitignore.template", + "package.json", + "tsconfig.json", + "build.mjs", + "src/app.ts", + "types/globals.d.ts", + }; + + /** + * Copies the TypeScript project template from classpath to the target directory. + */ + private static void copyTemplate(Path projectDir, String projectName) throws IOException { + Files.createDirectories(projectDir); + for (String relPath : TEMPLATE_FILES) { + // gitignore.template → .gitignore + 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 = Box3ScriptCommand.class.getResourceAsStream(resourcePath)) { + if (in == null) { + throw new IOException("Template file not found: " + resourcePath); + } + Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING); + } + // Replace placeholders in app.ts + if (relPath.equals("src/app.ts")) { + String content = Files.readString(dest); + content = content.replace("PROJECT_NAME", projectName); + Files.writeString(dest, content); + } + } + } + private static Path resolve(String input, MinecraftServer server) { Path p = Path.of(input); if (p.isAbsolute()) return p; 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..e4874fef --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/build.mjs @@ -0,0 +1,34 @@ +import * as esbuild from "esbuild"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; +import { writeFileSync, mkdirSync } from "fs"; +import babel from "@babel/core"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const srcDir = resolve(__dirname, "src"); +const distDir = resolve(__dirname, "dist"); + +const result = await esbuild.build({ + entryPoints: [resolve(srcDir, "app.ts")], + outfile: resolve(distDir, "app.js"), + bundle: true, + format: "cjs", + target: "rhino1.9.1", + platform: "neutral", + minify: false, + write: false, + supported: { + class: true, + }, +}); + +for (const out of result.outputFiles) { + let code = out.text; + const transformed = babel.transformSync(code, { + presets: [["@babel/preset-env", { targets: { ie: "11" }, modules: false }]], + configFile: false, + }); + code = transformed.code; + mkdirSync(dirname(out.path), { recursive: true }); + writeFileSync(out.path, code, "utf-8"); +} 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..deed335b --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/gitignore.template @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.env 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..d90db519 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/package.json @@ -0,0 +1,17 @@ +{ + "name": "box3js", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "node build.mjs", + "check": "tsc --noEmit", + "dev": "node build.mjs --watch" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/preset-env": "^7.29.2", + "esbuild": "^0.28.0", + "typescript": "^5.7.0" + } +} 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..338d55f3 --- /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: Entity) { + 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: Entity, 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..bf3ed845 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2015", + "module": "ES2015", + "moduleResolution": "bundler", + "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..78f68ef8 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/globals.d.ts @@ -0,0 +1,2220 @@ +// ================================================================ +// §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 DataStorage { + /** + * 获取存储空间名称 (只读)。 + * @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; +} + +/** + * 分页查询结果 (由 DataStorage.list() 返回)。 + * Paginated query result returned by DataStorage.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. + */ +interface StorageAPI { + /** 始终返回空字符串 (MC 本地存储无 key)。Always returns "" for MC local storage. */ + key: string; + + /** + * 打开或创建指定名称的数据存储空间。 + * Opens or creates a named data‑storage namespace. + * @param name - 命名空间 (可含 "/" 作为目录分隔) / namespace (may contain "/" as directory separator) + */ + getDataStorage(name: string): DataStorage; + + /** + * 行为与 getDataStorage 相同 (Box3 兼容别名)。 + * Same as getDataStorage — Box3 compatibility alias. + */ + getGroupStorage(name: string): DataStorage; +} + +// ================================================================ +// §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 Entity { + // ── 身份 / 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 Entity & { player: Player }; + + /** + * 实体类型标识符 (如 "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; + + // ── 无敌 & 持久化 / 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: Entity): void; + + /** 清除攻击目标, 停止追击。Clears the attack target, stopping pursuit. */ + clearTarget(): void; + + /** + * 获取当前攻击目标 (可能为 null)。 + * Returns the mob's current attack target, or null. + */ + getTarget(): Entity | 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: Entity) => void): void; + + // ── 玩家代理 / Player proxy ── + + /** + * 玩家接口 (仅当 isPlayer 为 true 时非 null)。 + * The player interface — non‑null only when isPlayer is true. + */ + player: Player | null; +} + +// ================================================================ +// §4 Player — 玩家 +// ================================================================ + +/** + * 玩家扩展接口 (通过 entity.player 访问)。 + * Player‑specific interface — accessed via `entity.player`. + */ +interface Player { + // ── 身份 / 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: Entity | 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; + + // ── 经验 / 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: Entity, message: string, tick: number) => void, + ): void; +} + +// ================================================================ +// §5 World — 世界 API +// ================================================================ + +/** + * 世界控制与事件 — 脚本中通过 `world` 访问。 + * World control & events — accessed via `world` in scripts. + */ +interface World { + // ── 世界属性 / 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): Entity[]; + + /** + * 查询第一个匹配的实体 (或 null)。 + * Selects the first matching entity, or null. + */ + querySelector(selector: string): Entity | null; + + /** + * 查询指定区域内的所有实体。 + * Returns all entities inside an AABB defined by two corners. + */ + entitiesInArea(pos1: GameVector3, pos2: GameVector3): Entity[]; + + /** + * 查询指定半径内的所有实体。 + * Returns all entities within a radius around a point. + */ + entitiesInRadius(x: number, y: number, z: number, radius: number): Entity[]; + entitiesInRadius(pos: GameVector3, radius: number): Entity[]; + + // ── 实体生成 / Entity Spawning ── + + /** + * 在指定位置生成实体。 + * Spawns an entity at the given position. + * @param type - 实体类型 ID (如 "minecraft:zombie") + * @param pos - 生成坐标 + * @returns 生成的实体包装, 失败返回 null + */ + spawnEntity(type: string, pos: GameVector3): Entity | 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, + ): Entity | null; + launchProjectile( + type: string, + pos: GameVector3, + target: GameVector3, + speed: number, + ): Entity | 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 | Entity, + objectiveName: string, + value: number, + ): void; + + /** + * 获取分数。 + * Gets the score of an entity or name for a given objective. + */ + getScore(entityOrName: string | Entity, 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 | Entity, teamName: string): void; + + /** + * 将实体/名称移出队伍。 + * Removes an entity or name from its current team. + */ + leaveTeam(entityOrName: string | Entity): void; + + /** + * 获取实体/名称所在的队伍名 (不在任何队伍返回 null)。 + * Returns the team name of an entity or name, or null. + */ + getTeamOf(entityOrName: string | Entity): 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: Entity) => void): void; + + /** + * 注册玩家离开回调。 + * Registers a callback invoked when a player leaves the server. + */ + onPlayerLeave(handler: (entity: Entity) => void): void; + + /** + * 注册聊天消息回调 (包括 /me 消息)。 + * Registers a callback for chat messages (including /me). + * @param handler - (entity, message, tick) => void + */ + onChat( + handler: (entity: Entity, message: string, tick: number) => void, + ): void; + + /** + * 注册玩家重生回调。 + * Registers a callback invoked when a player respawns. + */ + onPlayerRespawn(handler: (entity: Entity) => void): void; + + /** + * 注册方块右键激活回调。 + * Registers a callback invoked when a player right‑clicks a block. + */ + onBlockActivate( + handler: ( + entity: Entity, + 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: Entity, + 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: Entity, + 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: Entity, + 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: Entity, target: Entity, tick: number) => void, + ): void; + + /** + * 注册实体死亡回调。 + * Registers a callback invoked when an entity dies. + */ + onEntityDeath( + handler: (entity: Entity, killer: Entity | null, tick: number) => void, + ): void; + + /** + * 注册实体受伤回调。 + * Registers a callback invoked when an entity takes damage. + */ + onEntityDamage( + handler: ( + entity: Entity, + amount: number, + source: string, + attacker: Entity | null, + tick: number, + ) => void, + ): void; + + /** + * 注册流体进入回调 (玩家进入水/熔岩)。 + * Registers a callback invoked when a player enters a fluid. + */ + onFluidEnter( + handler: ( + entity: Entity, + 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: Entity, + 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: Entity, entityB: Entity, tick: number) => void, + ): void; + + /** + * 注册实体分离回调 (两个实体不再碰撞)。 + * Registers a callback invoked when two entities separate after contact. + */ + onEntitySeparate( + handler: (entityA: Entity, entityB: Entity, 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?: Entity; +} + +// ================================================================ +// §6 Voxels — 方块操作 +// ================================================================ + +/** + * 方块读写操作 — 脚本中通过 `voxels` 访问。 + * Voxel (block) read/write — accessed via `voxels` in scripts. + * + * @remarks + * 所有坐标使用世界方块坐标 (整数)。 + * All coordinates are in world block space (integers). + */ +interface Voxels { + // ── 世界尺寸 / 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 Console { + /** 普通日志。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: World; + +/** 方块读写 API / Block read & write */ +declare const voxels: Voxels; + +/** 持久化存储 API / Persistent key‑value storage */ +declare const storage: StorageAPI; + +/** 服务端控制台输出 / Server console output */ +declare const console: Console; + +/** + * 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; From 55d578547e5dc3b5d5551b4b3756c54852c93a1b Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Wed, 6 May 2026 10:40:18 +0800 Subject: [PATCH 10/17] =?UTF-8?q?feat:=20=E7=A7=BB=E9=99=A4=20eval=20?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E5=B9=B6=E5=A2=9E=E5=BC=BA=20TypeScript=20?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: 出于安全原因移除了 `/box3script eval ` 命令 - 更新 `/box3script create ` 命令,生成包含完整项目结构的 TypeScript 脚手架 - 模板改为使用 GameEntity 替代 Entity 接口 - 将接口从 Entity 重命名为 GameEntity,World 重命名为 GameWorld, StorageAPI 重命名为 GameStorage,Player 重命名为 GamePlayer, Voxels 重命名为 GameVoxels,Console 重命名为 GameConsole - 更新文档以反映新的 TypeScript 工作流和项目结构 --- Box3JS-NeoForge-1.21.1/README.md | 1 - Box3JS-NeoForge-1.21.1/docs/api/commands.md | 39 ++++---- .../box3js/script/Box3ScriptCommand.java | 18 ---- .../assets/box3js/template/src/app.ts | 4 +- .../assets/box3js/template/types/globals.d.ts | 94 +++++++++---------- 5 files changed, 72 insertions(+), 84 deletions(-) diff --git a/Box3JS-NeoForge-1.21.1/README.md b/Box3JS-NeoForge-1.21.1/README.md index db2b9e60..933cfac3 100644 --- a/Box3JS-NeoForge-1.21.1/README.md +++ b/Box3JS-NeoForge-1.21.1/README.md @@ -93,7 +93,6 @@ npm run build # 输出 dist/app.js | `/box3script off ` | 禁用项目 | | `/box3script reload` | 重载所有已启用脚本 | | `/box3script stop` | 停止所有脚本 | -| `/box3script eval ` | 直接执行 JS 代码 | | `/box3script file ` | 加载 JS 文件 | [命令详细参考 →](docs/api/commands.md) diff --git a/Box3JS-NeoForge-1.21.1/docs/api/commands.md b/Box3JS-NeoForge-1.21.1/docs/api/commands.md index 1e705997..2aa5ee6e 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/commands.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/commands.md @@ -6,15 +6,6 @@ ## 命令列表 -### `/box3script eval ` - -直接执行一段 JS 代码。 - -``` -/box3script eval world.say("hello") -/box3script eval var p = world.querySelectorAll("*")[0].player; p.teleport(new GameVector3(0,100,0)) -``` - ### `/box3script file ` 加载并执行服务器上的 JS 文件。支持相对路径(相对于 `config/box3/script/`)和绝对路径。 @@ -34,7 +25,7 @@ ### `/box3script create ` -创建新的脚本项目。在 `config/box3/script//` 下创建目录和 `app.js` 模板文件。创建后默认**禁用**。 +创建新的 TypeScript 脚本项目。在 `config/box3/script//` 下生成完整的 TS 脚手架。创建后默认**禁用**。 ``` /box3script create mygame @@ -44,7 +35,22 @@ ``` config/box3/script/ └── mygame/ - └── app.js ← 模板脚本 + ├── .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 list` @@ -134,12 +140,13 @@ config/box3/ ├── scripts.json ← 项目开关配置 ├── script/ ← 脚本目录 │ ├── skyrun/ - │ │ └── app.js ← 天空跑酷 - │ ├── siege/ - │ │ └── app.js ← 围攻游戏 + │ │ ├── package.json + │ │ ├── src/app.ts + │ │ └── dist/app.js ← 编译产物 │ └── mygame/ - │ └── app.js ← 自定义项目 + │ ├── package.json + │ ├── src/app.ts + │ └── dist/app.js └── data/ ← 存储数据目录 (storage API) - ├── skyrun_times/ └── ... ``` 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 index fa5e98c4..9f239791 100644 --- 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 @@ -23,24 +23,6 @@ public static void register(RegisterCommandsEvent event) { dispatcher.register( literal("box3script") .requires(src -> src.hasPermission(2)) - // --- eval --- - .then(literal("eval") - .then(argument("code", StringArgumentType.greedyString()) - .executes(ctx -> { - String code = StringArgumentType.getString(ctx, "code"); - var server = ctx.getSource().getServer(); - try { - Box3ScriptEngine.get().init(server); - Box3ScriptEngine.get().eval(code); - ctx.getSource().sendSuccess( - () -> Component.literal("Script executed."), false); - } catch (Exception e) { - ctx.getSource().sendFailure( - Component.literal("Script error: " + e.getMessage())); - e.printStackTrace(); - } - return 1; - }))) // --- file --- .then(literal("file") .then(argument("path", StringArgumentType.greedyString()) 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 index 338d55f3..040a2c90 100644 --- 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 @@ -3,7 +3,7 @@ // ═══════════════════════════════════════════════════ // 玩家加入时欢迎 -world.onPlayerJoin(function (entity: Entity) { +world.onPlayerJoin(function (entity: GameEntity) { var p = entity.player; if (!p) return; world.say("§e" + p.name + " §7进入了服务器"); @@ -12,7 +12,7 @@ world.onPlayerJoin(function (entity: Entity) { }); // 聊天命令 -world.onChat(function (entity: Entity, message: string, _tick: number) { +world.onChat(function (entity: GameEntity, message: string, _tick: number) { var p = entity.player; if (!p) return; 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 index 78f68ef8..f499a22a 100644 --- 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 @@ -529,7 +529,7 @@ interface ReturnValue { * 全局存储入口 — 脚本中通过 `storage` 访问。 * Global storage entry point — accessed via `storage` in scripts. */ -interface StorageAPI { +interface GameStorage { /** 始终返回空字符串 (MC 本地存储无 key)。Always returns "" for MC local storage. */ key: string; @@ -559,7 +559,7 @@ interface StorageAPI { * 通过 `world.querySelector()`, `world.querySelectorAll()` 或事件回调获取。 * Obtained via `world.querySelector()`, `world.querySelectorAll()`, or event callbacks. */ -interface Entity { +interface GameEntity { // ── 身份 / Identity ── /** @@ -572,7 +572,7 @@ interface Entity { * 是否为玩家实体。返回 true 后 player 属性自动收窄为非 null。 * True if this entity is a player. After a truthy check, `player` is narrowed to non-null. */ - isPlayer(): this is Entity & { player: Player }; + isPlayer(): this is GameEntity & { player: GamePlayer }; /** * 实体类型标识符 (如 "minecraft:zombie")。 @@ -770,7 +770,7 @@ interface Entity { * 设置生物的当前攻击目标。 * Sets the mob's attack target (the mob will pathfind to and attack it). */ - setTarget(target: Entity): void; + setTarget(target: GameEntity): void; /** 清除攻击目标, 停止追击。Clears the attack target, stopping pursuit. */ clearTarget(): void; @@ -779,7 +779,7 @@ interface Entity { * 获取当前攻击目标 (可能为 null)。 * Returns the mob's current attack target, or null. */ - getTarget(): Entity | null; + getTarget(): GameEntity | null; /** * 启用或禁用生物 AI (寻路/目标等)。 @@ -814,7 +814,7 @@ interface Entity { * 注册实体被销毁时的回调。 * Registers a callback to be called when this entity is destroyed. */ - setOnDestroy(handler: (entity: Entity) => void): void; + setOnDestroy(handler: (entity: GameEntity) => void): void; // ── 玩家代理 / Player proxy ── @@ -822,7 +822,7 @@ interface Entity { * 玩家接口 (仅当 isPlayer 为 true 时非 null)。 * The player interface — non‑null only when isPlayer is true. */ - player: Player | null; + player: GamePlayer | null; } // ================================================================ @@ -833,7 +833,7 @@ interface Entity { * 玩家扩展接口 (通过 entity.player 访问)。 * Player‑specific interface — accessed via `entity.player`. */ -interface Player { +interface GamePlayer { // ── 身份 / Identity ── /** 玩家名。Player display name. */ @@ -937,7 +937,7 @@ interface Player { * 相机跟随的实体 (在 FOLLOW 模式下)。 * The entity the camera follows (when in FOLLOW mode). */ - cameraEntity: Entity | null; + cameraEntity: GameEntity | null; /** 相机俯仰角。Camera pitch (vertical rotation). */ cameraPitch: number; @@ -1170,7 +1170,7 @@ interface Player { * Registers a per‑player chat handler (overrides global onChat for this player). */ onChat( - handler: (entity: Entity, message: string, tick: number) => void, + handler: (entity: GameEntity, message: string, tick: number) => void, ): void; } @@ -1182,7 +1182,7 @@ interface Player { * 世界控制与事件 — 脚本中通过 `world` 访问。 * World control & events — accessed via `world` in scripts. */ -interface World { +interface GameWorld { // ── 世界属性 / World properties ── /** 服务器 MOTD。Server MOTD string. */ @@ -1313,26 +1313,26 @@ interface World { * Selects all entities matching a selector (currently only players). * @param selector - "*" (所有玩家) | "#uuid" | ".tag" */ - querySelectorAll(selector: string): Entity[]; + querySelectorAll(selector: string): GameEntity[]; /** * 查询第一个匹配的实体 (或 null)。 * Selects the first matching entity, or null. */ - querySelector(selector: string): Entity | null; + querySelector(selector: string): GameEntity | null; /** * 查询指定区域内的所有实体。 * Returns all entities inside an AABB defined by two corners. */ - entitiesInArea(pos1: GameVector3, pos2: GameVector3): Entity[]; + entitiesInArea(pos1: GameVector3, pos2: GameVector3): GameEntity[]; /** * 查询指定半径内的所有实体。 * Returns all entities within a radius around a point. */ - entitiesInRadius(x: number, y: number, z: number, radius: number): Entity[]; - entitiesInRadius(pos: GameVector3, radius: number): Entity[]; + entitiesInRadius(x: number, y: number, z: number, radius: number): GameEntity[]; + entitiesInRadius(pos: GameVector3, radius: number): GameEntity[]; // ── 实体生成 / Entity Spawning ── @@ -1343,7 +1343,7 @@ interface World { * @param pos - 生成坐标 * @returns 生成的实体包装, 失败返回 null */ - spawnEntity(type: string, pos: GameVector3): Entity | null; + spawnEntity(type: string, pos: GameVector3): GameEntity | null; // ── 射线检测 / Raycast ── @@ -1507,13 +1507,13 @@ interface World { ty: number, tz: number, speed: number, - ): Entity | null; + ): GameEntity | null; launchProjectile( type: string, pos: GameVector3, target: GameVector3, speed: number, - ): Entity | null; + ): GameEntity | null; // ── 计分板 / Scoreboard ── @@ -1537,7 +1537,7 @@ interface World { * Sets the score of an entity or name for a given objective. */ setScore( - entityOrName: string | Entity, + entityOrName: string | GameEntity, objectiveName: string, value: number, ): void; @@ -1546,7 +1546,7 @@ interface World { * 获取分数。 * Gets the score of an entity or name for a given objective. */ - getScore(entityOrName: string | Entity, objectiveName: string): number; + getScore(entityOrName: string | GameEntity, objectiveName: string): number; /** * 在指定显示位置展示计分板。 @@ -1605,19 +1605,19 @@ interface World { * 将实体/名称加入队伍。 * Adds an entity or name to a team. */ - joinTeam(entityOrName: string | Entity, teamName: string): void; + joinTeam(entityOrName: string | GameEntity, teamName: string): void; /** * 将实体/名称移出队伍。 * Removes an entity or name from its current team. */ - leaveTeam(entityOrName: string | Entity): void; + leaveTeam(entityOrName: string | GameEntity): void; /** * 获取实体/名称所在的队伍名 (不在任何队伍返回 null)。 * Returns the team name of an entity or name, or null. */ - getTeamOf(entityOrName: string | Entity): string | null; + getTeamOf(entityOrName: string | GameEntity): string | null; // ── 世界边界 / World Border ── @@ -1700,13 +1700,13 @@ interface World { * 注册玩家加入回调。 * Registers a callback invoked when a player joins the server. */ - onPlayerJoin(handler: (entity: Entity) => void): void; + onPlayerJoin(handler: (entity: GameEntity) => void): void; /** * 注册玩家离开回调。 * Registers a callback invoked when a player leaves the server. */ - onPlayerLeave(handler: (entity: Entity) => void): void; + onPlayerLeave(handler: (entity: GameEntity) => void): void; /** * 注册聊天消息回调 (包括 /me 消息)。 @@ -1714,14 +1714,14 @@ interface World { * @param handler - (entity, message, tick) => void */ onChat( - handler: (entity: Entity, message: string, tick: number) => void, + handler: (entity: GameEntity, message: string, tick: number) => void, ): void; /** * 注册玩家重生回调。 * Registers a callback invoked when a player respawns. */ - onPlayerRespawn(handler: (entity: Entity) => void): void; + onPlayerRespawn(handler: (entity: GameEntity) => void): void; /** * 注册方块右键激活回调。 @@ -1729,7 +1729,7 @@ interface World { */ onBlockActivate( handler: ( - entity: Entity, + entity: GameEntity, x: number, y: number, z: number, @@ -1744,7 +1744,7 @@ interface World { */ onVoxelDestroy( handler: ( - entity: Entity, + entity: GameEntity, x: number, y: number, z: number, @@ -1759,7 +1759,7 @@ interface World { */ onBlockPlace( handler: ( - entity: Entity, + entity: GameEntity, x: number, y: number, z: number, @@ -1775,7 +1775,7 @@ interface World { */ onVoxelContact( handler: ( - entity: Entity, + entity: GameEntity, voxelId: number, x: number, y: number, @@ -1791,7 +1791,7 @@ interface World { * Registers a callback invoked when a player right‑clicks an entity. */ onInteract( - handler: (entity: Entity, target: Entity, tick: number) => void, + handler: (entity: GameEntity, target: GameEntity, tick: number) => void, ): void; /** @@ -1799,7 +1799,7 @@ interface World { * Registers a callback invoked when an entity dies. */ onEntityDeath( - handler: (entity: Entity, killer: Entity | null, tick: number) => void, + handler: (entity: GameEntity, killer: GameEntity | null, tick: number) => void, ): void; /** @@ -1808,10 +1808,10 @@ interface World { */ onEntityDamage( handler: ( - entity: Entity, + entity: GameEntity, amount: number, source: string, - attacker: Entity | null, + attacker: GameEntity | null, tick: number, ) => void, ): void; @@ -1822,7 +1822,7 @@ interface World { */ onFluidEnter( handler: ( - entity: Entity, + entity: GameEntity, fluid: string, x: number, y: number, @@ -1837,7 +1837,7 @@ interface World { */ onFluidLeave( handler: ( - entity: Entity, + entity: GameEntity, fluid: string, x: number, y: number, @@ -1851,7 +1851,7 @@ interface World { * Registers a callback invoked when two entities come into contact. */ onEntityContact( - handler: (entityA: Entity, entityB: Entity, tick: number) => void, + handler: (entityA: GameEntity, entityB: GameEntity, tick: number) => void, ): void; /** @@ -1859,7 +1859,7 @@ interface World { * Registers a callback invoked when two entities separate after contact. */ onEntitySeparate( - handler: (entityA: Entity, entityB: Entity, tick: number) => void, + handler: (entityA: GameEntity, entityB: GameEntity, tick: number) => void, ): void; /** @@ -1893,7 +1893,7 @@ interface RaycastResult { /** 命中的方块 ID (命中方块时为数字)。Hit block ID (number when a block was hit). */ voxel?: number; /** 命中的实体 (命中实体时)。The entity that was hit (when an entity was hit). */ - entity?: Entity; + entity?: GameEntity; } // ================================================================ @@ -1908,7 +1908,7 @@ interface RaycastResult { * 所有坐标使用世界方块坐标 (整数)。 * All coordinates are in world block space (integers). */ -interface Voxels { +interface GameVoxels { // ── 世界尺寸 / World dimensions ── /** @@ -2072,7 +2072,7 @@ interface Voxels { * 输出格式: [Box3JS] [projectName] * 会通过 System.out / System.err 输出到服务端控制台。 */ -interface Console { +interface GameConsole { /** 普通日志。Info‑level log. */ log(...args: unknown[]): void; @@ -2183,16 +2183,16 @@ declare const GamePlayerWalkState: { // ================================================================ /** 世界控制与事件 API / World control & events */ -declare const world: World; +declare const world: GameWorld; /** 方块读写 API / Block read & write */ -declare const voxels: Voxels; +declare const voxels: GameVoxels; /** 持久化存储 API / Persistent key‑value storage */ -declare const storage: StorageAPI; +declare const storage: GameStorage; /** 服务端控制台输出 / Server console output */ -declare const console: Console; +declare const console: GameConsole; /** * CommonJS 模块导入。 From a49349913844597319d0539792e4d26085b6db97 Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Wed, 6 May 2026 11:00:03 +0800 Subject: [PATCH 11/17] =?UTF-8?q?feat(script):=20=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=88=9B=E5=BB=BA=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8F=AF=E7=82=B9=E5=87=BB=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E5=B9=B6=E6=94=B9=E8=BF=9B=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加带有复制到剪贴板功能的可点击路径组件 - 移除 box3script 中已弃用的 'file' 命令 - 移除 box3script 中已弃用的 'run' 命令 - 添加使用 'off ' 子命令禁用特定项目的功能 - 改进项目创建成功消息的格式 refactor(types): 将 DataStorage 重命名为 GameDataStorage 以提高清晰度 - 将 globals.d.ts 中的 DataStorage 接口重命名为 GameDataStorage - 更新所有从 DataStorage 到 GameDataStorage 的引用 - 更新文档注释以反映新的接口名称 --- .../box3js/script/Box3ScriptCommand.java | 89 ++++++------------- .../assets/box3js/template/types/globals.d.ts | 10 +-- 2 files changed, 30 insertions(+), 69 deletions(-) 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 index 9f239791..54d1ac72 100644 --- 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 @@ -1,8 +1,9 @@ package com.box3lab.box3js.script; import com.mojang.brigadier.arguments.StringArgumentType; -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.minecraft.server.MinecraftServer; import net.neoforged.neoforge.event.RegisterCommandsEvent; @@ -23,38 +24,6 @@ public static void register(RegisterCommandsEvent event) { dispatcher.register( literal("box3script") .requires(src -> src.hasPermission(2)) - // --- file --- - .then(literal("file") - .then(argument("path", StringArgumentType.greedyString()) - .executes(ctx -> { - String input = StringArgumentType.getString(ctx, "path"); - CommandSourceStack src = ctx.getSource(); - var server = src.getServer(); - Path filePath = resolve(input, server); - if (!Files.exists(filePath)) { - src.sendFailure(Component.literal("File not found: " + filePath)); - return 0; - } - try { - Box3ScriptEngine.get().init(server); - // Detect project name for require() support - Path scriptDir = server.getServerDirectory().resolve("config/box3/script"); - Path relative = null; - try { relative = scriptDir.relativize(filePath.toAbsolutePath()); } catch (Exception ignored) {} - if (relative != null && relative.getNameCount() > 1) { - Box3ScriptEngine.get().setCurrentProject(relative.getName(0).toString()); - } - Box3ScriptEngine.get().eval(Files.readString(filePath)); - src.sendSuccess( - () -> Component.literal("Executed: " + filePath.getFileName()), false); - } catch (IOException e) { - src.sendFailure(Component.literal("Failed to read file: " + e.getMessage())); - } catch (Exception e) { - src.sendFailure(Component.literal("Script error: " + e.getMessage())); - e.printStackTrace(); - } - return 1; - }))) // --- create --- .then(literal("create") .then(argument("name", StringArgumentType.word()) @@ -68,12 +37,16 @@ public static void register(RegisterCommandsEvent event) { } try { copyTemplate(projectDir, name); - ctx.getSource().sendSuccess( - () -> Component.literal("Project created: " + name - + "\n cd config/box3/script/" + name + String absPath = projectDir.toAbsolutePath().toString(); + Component msg = Component.literal("Project created: " + name + "\n") + .append(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"))))) + .append(Component.literal(" cd config/box3/script/" + name + "\n npm install && npm run build" - + "\nUse /box3script on " + name + " to enable it."), - false); + + "\nUse /box3script on " + name + " to enable it.")); + ctx.getSource().sendSuccess(() -> msg, false); } catch (IOException e) { ctx.getSource().sendFailure( Component.literal("Failed to create: " + e.getMessage())); @@ -85,10 +58,22 @@ public static void register(RegisterCommandsEvent event) { .executes(ctx -> { Box3ScriptEngine.get().reset(); ctx.getSource().sendSuccess( - () -> Component.literal("All scripts stopped. Callbacks cleared, scope reset."), + () -> Component.literal("All scripts stopped."), false); return 1; - })) + }) + .then(argument("project", StringArgumentType.word()) + .executes(ctx -> { + String project = StringArgumentType.getString(ctx, "project"); + Box3ScriptConfig.get().setEnabled(project, false); + var server = ctx.getSource().getServer(); + Box3ScriptEngine.get().reset(); + Box3ScriptEngine.get().autoLoad(server); + ctx.getSource().sendSuccess( + () -> Component.literal("Stopped and disabled: " + project), + false); + return 1; + }))) // --- list --- .then(literal("list") .executes(ctx -> { @@ -162,30 +147,6 @@ public static void register(RegisterCommandsEvent event) { false); return 1; })) - // --- run --- - .then(literal("run") - .then(argument("project", StringArgumentType.greedyString()) - .executes(ctx -> { - String project = StringArgumentType.getString(ctx, "project"); - CommandSourceStack src = ctx.getSource(); - var server = src.getServer(); - Path appJs = resolve(project, server).resolve("app.js"); - if (!Files.exists(appJs)) { - src.sendFailure(Component.literal("app.js not found: " + appJs)); - return 0; - } - try { - Box3ScriptEngine.get().init(server); - Box3ScriptEngine.get().setCurrentProject(project); - Box3ScriptEngine.get().eval("require('./app')"); - src.sendSuccess( - () -> Component.literal("Executed: " + project + "/app.js"), false); - } catch (Exception e) { - src.sendFailure(Component.literal("Script error: " + e.getMessage())); - e.printStackTrace(); - } - return 1; - }))) ); } 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 index f499a22a..cb160aab 100644 --- 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 @@ -403,7 +403,7 @@ interface AxisAngle { * 通过 `storage.getDataStorage("name")` 获取。 * Obtain via `storage.getDataStorage("name")`. */ -interface DataStorage { +interface GameDataStorage { /** * 获取存储空间名称 (只读)。 * @en Returns the read‑only namespace name. @@ -488,8 +488,8 @@ interface DataStorage { } /** - * 分页查询结果 (由 DataStorage.list() 返回)。 - * Paginated query result returned by DataStorage.list(). + * 分页查询结果 (由 GameDataStorage.list() 返回)。 + * Paginated query result returned by GameDataStorage.list(). */ interface QueryList { /** 是否已到达最后一页。Whether the last page has been reached. */ @@ -538,13 +538,13 @@ interface GameStorage { * Opens or creates a named data‑storage namespace. * @param name - 命名空间 (可含 "/" 作为目录分隔) / namespace (may contain "/" as directory separator) */ - getDataStorage(name: string): DataStorage; + getDataStorage(name: string): GameDataStorage; /** * 行为与 getDataStorage 相同 (Box3 兼容别名)。 * Same as getDataStorage — Box3 compatibility alias. */ - getGroupStorage(name: string): DataStorage; + getGroupStorage(name: string): GameDataStorage; } // ================================================================ From c747ddd8f776fa4004146b8cab5a8e630439497d Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Wed, 6 May 2026 12:59:21 +0800 Subject: [PATCH 12/17] =?UTF-8?q?docs(api):=20=E6=9B=B4=E6=96=B0=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E6=96=87=E6=A1=A3=E5=B9=B6=E5=B0=86=20data=20?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E9=87=8D=E5=91=BD=E5=90=8D=E4=B8=BA=20storag?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除已弃用的 `/box3script file` 和 `/box3script run` 命令 - 更新 `/box3script on` 命令描述,反映立即执行的行为 - 添加 `/box3script watch` 命令,支持文件监控和热重载 - 添加通过 `/box3script stop ` 停止特定项目的功能 - 将文档中的 data 目录从 `data/` 重命名为 `storage/` docs(storage): 增强 storage API 文档,添加缓存细节说明 - 更新 storage API 以反映内存缓存能力 - 阐明跨项目共享存储功能 - 添加内存缓存和持久化章节,说明并发访问 - 更新示例,使用 getGroupStorage 实现共享排行榜 refactor(script): 通过提取工具方法优化 Box3JSEntity - 将内联注册表查找替换为 Box3ScriptUtils 辅助方法 - 将 LivingEntity 类型转换提取为私有 asLiving() 方法 - 将 lookAt 实现移至工具类以减少重复代码 - 移除未使用的导入(ResourceLocation、UUID、BuiltInRegistries) refactor(player): 精简玩家能力更新和注册表查找 - 引入 updateAbility() 辅助方法处理玩家能力 - 将直接注册表查找替换为 Box3ScriptUtils 查找方法 - 将 lookAt 实现移至工具类 - 添加缺失的 Consumer 导入 feat(storage): 为存储操作实现内存缓存 - 为 JSON 存储文件添加基于 ConcurrentHashMap 的缓存 - 实现项目命名空间解析以实现数据隔离 - 添加对缓存数据映射的同步访问 - 通过减少磁盘 I/O 操作提升性能 refactor(voxels): 使用工具方法替换注册表查找 - 使用 Box3ScriptUtils.lookupBlock 替代直接注册表访问 - 将 ResourceLocation 解析替换为工具查找方法 - 简化刷怪笼功能中的实体类型查找 - 移除冗余的注册表访问代码 --- Box3JS-NeoForge-1.21.1/docs/api/commands.md | 43 +- Box3JS-NeoForge-1.21.1/docs/api/storage.md | 19 +- .../box3lab/box3js/script/Box3JSBossbar.java | 54 ++ .../box3js/script/Box3JSCallbacks.java | 81 +++ .../box3lab/box3js/script/Box3JSEntity.java | 75 +-- .../box3lab/box3js/script/Box3JSEventBus.java | 140 ++++ .../box3lab/box3js/script/Box3JSPlayer.java | 52 +- .../box3lab/box3js/script/Box3JSQuery.java | 150 +++++ .../box3js/script/Box3JSScoreboard.java | 95 +++ .../box3lab/box3js/script/Box3JSStorage.java | 156 +++-- .../com/box3lab/box3js/script/Box3JSTeam.java | 55 ++ .../box3lab/box3js/script/Box3JSVoxels.java | 23 +- .../box3lab/box3js/script/Box3JSWorld.java | 621 +++--------------- .../box3js/script/Box3ScriptCommand.java | 384 ++++++----- .../box3js/script/Box3ScriptEngine.java | 543 +++++++++------ .../box3js/script/Box3ScriptTemplate.java | 37 ++ .../box3js/script/Box3ScriptUtils.java | 85 +++ .../box3js/script/Box3ScriptWatcher.java | 157 +++++ .../assets/box3js/template/types/globals.d.ts | 19 +- 19 files changed, 1683 insertions(+), 1106 deletions(-) create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSBossbar.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSCallbacks.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEventBus.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSQuery.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSScoreboard.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSTeam.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptUtils.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptWatcher.java diff --git a/Box3JS-NeoForge-1.21.1/docs/api/commands.md b/Box3JS-NeoForge-1.21.1/docs/api/commands.md index 2aa5ee6e..8eff9a0f 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/commands.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/commands.md @@ -6,23 +6,6 @@ ## 命令列表 -### `/box3script file ` - -加载并执行服务器上的 JS 文件。支持相对路径(相对于 `config/box3/script/`)和绝对路径。 - -``` -/box3script file my_script.js -/box3script file /home/server/scripts/test.js -``` - -### `/box3script run ` - -运行一次指定项目的 `app.js`(不改变启用状态)。 - -``` -/box3script run skyrun -``` - ### `/box3script create ` 创建新的 TypeScript 脚本项目。在 `config/box3/script//` 下生成完整的 TS 脚手架。创建后默认**禁用**。 @@ -71,7 +54,7 @@ npm run build # 输出 dist/app.js ### `/box3script on ` -启用指定项目。下次服务端重启时自动执行该项目的 `app.js`。 +启用指定项目并**立即加载执行**。加载错误会直接反馈到聊天栏。 ``` /box3script on skyrun @@ -103,20 +86,38 @@ npm run build # 输出 dist/app.js ### `/box3script reload` -停止所有脚本,重新加载所有已启用项目的 `app.js`。等价于 `stop` + 重新 `autoLoad`。 +停止所有脚本,重新加载所有已启用项目的 `app.js`。等价于 `stop` + 重新 `autoLoad`。加载错误会反馈到聊天栏。 ``` /box3script reload ``` +### `/box3script watch` + +开启/关闭文件监控。开启后监控所有项目的 `dist/` 目录,`.js` 文件变化时自动热重载对应项目(2 秒防抖)。 + +``` +/box3script watch # 切换 开/关 +/box3script watch on # 开启 +/box3script watch off # 关闭 +``` + ### `/box3script stop` -立即停止所有正在运行的脚本。清除所有回调、定时器和作用域。 +停止所有项目,清除全部回调、定时器和作用域。 ``` /box3script stop ``` +### `/box3script stop ` + +停止指定项目,仅清除该项目的回调、定时器和作用域,**不影响其他正在运行的项目**。 + +``` +/box3script stop siege +``` + --- ## 配置文件 @@ -147,6 +148,6 @@ config/box3/ │ ├── package.json │ ├── src/app.ts │ └── dist/app.js - └── data/ ← 存储数据目录 (storage API) + └── storage/ ← 存储数据目录 (storage API) └── ... ``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/storage.md b/Box3JS-NeoForge-1.21.1/docs/api/storage.md index 29309252..ff2e8389 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/storage.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/storage.md @@ -1,6 +1,6 @@ # storage — 数据存储 API -`storage` 提供 JSON 文件持久化存储,项目间数据隔离。数据保存在 `config/box3/data/<项目名>/` 目录下。 +`storage` 提供 JSON 文件持久化存储,带内存缓存加速读写。数据保存在 `config/box3/storage/<项目名>/` 目录下,每个项目自动拥有独立命名空间。 --- @@ -12,7 +12,7 @@ ### storage.getGroupStorage(name) -✅ Box3 API | 获取组存储(目前与 `getDataStorage` 行为一致)。 +✅ Box3 API | 获取**跨项目共享**存储。所有项目通过同一 `name` 访问同一份数据(底层使用 `__shared__/` 命名空间)。适合做全服排行榜、全局配置等。 ```js var store = storage.getDataStorage("leaderboard"); @@ -79,7 +79,7 @@ store.update("counter", function(current) { ### store.destroy() -✅ Box3 API | 删除整个存储文件。 +✅ Box3 API | 删除整个存储文件(同时清除内存缓存)。 ```js store.remove("tempKey"); @@ -145,10 +145,21 @@ if (!result.isLastPage) { --- +## 内存缓存与持久化 + +所有 `GameDataStorage` 实例共享一个内存缓存(`ConcurrentHashMap`)。首次访问时从磁盘加载 JSON,后续读写均在内存中操作,每次写操作(`set`/`update`/`remove`/`increment`)同步刷盘。 + +- **同名存储**:同一文件路径多次 `getDataStorage` 返回共享同一份内存数据,避免重复 I/O +- **项目隔离**:`getDataStorage("scores")` 在不同项目中访问不同文件(自动添加项目名前缀) +- **跨项目共享**:`getGroupStorage("leaderboard")` 所有项目访问同一个 `__shared__/leaderboard.json` + +--- + ## 完整示例:排行榜 ```js -var lb = storage.getDataStorage("leaderboard"); +// 跨项目共享排行榜 — 所有项目读写同一份数据 +var lb = storage.getGroupStorage("leaderboard"); // 保存成绩 function saveScore(name, time) { 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..8a4e6e0f --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSBossbar.java @@ -0,0 +1,54 @@ +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.world.entity.player.Player; +import net.minecraft.server.level.ServerBossEvent; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +class Box3JSBossbar { + + private final MinecraftServer server; + private final Map bossBars = new HashMap<>(); + + Box3JSBossbar(MinecraftServer server) { + this.server = server; + } + + void showBossbar(String name, String text, double progress, String colorName) { + ServerBossEvent bar = bossBars.get(name); + if (bar == null) { + bar = new ServerBossEvent(Component.literal(text), resolveColor(colorName), BossBarOverlay.PROGRESS); + bossBars.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 name) { + ServerBossEvent bar = bossBars.remove(name); + if (bar != null) bar.removeAllPlayers(); + } + + 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 index ac784ce9..18d8756a 100644 --- 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 @@ -1,8 +1,6 @@ package com.box3lab.box3js.script; import net.minecraft.core.Holder; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.effect.MobEffect; @@ -17,7 +15,6 @@ import org.mozilla.javascript.Function; import java.util.Map; -import java.util.UUID; import java.util.function.Consumer; public class Box3JSEntity { @@ -135,24 +132,28 @@ public GameVector3 getEyePosition() { public boolean getDestroyed() { return entity.isRemoved(); } public double getHp() { - if (entity instanceof LivingEntity le) return le.getHealth(); + LivingEntity le = asLiving(); + if (le != null) return le.getHealth(); return getProp("hp", 100.0); } public void setHp(double v) { setProp("hp", v); - if (entity instanceof LivingEntity le) { + LivingEntity le = asLiving(); + if (le != null) { double max = le.getMaxHealth(); le.setHealth((float) Math.max(0, Math.min(v, max))); } } public double getMaxHp() { - if (entity instanceof LivingEntity le) return le.getMaxHealth(); + LivingEntity le = asLiving(); + if (le != null) return le.getMaxHealth(); return getProp("maxHp", 100.0); } public void setMaxHp(double v) { setProp("maxHp", v); - if (entity instanceof LivingEntity le) { + 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); @@ -160,15 +161,17 @@ public void setMaxHp(double v) { } public void hurt(double amount) { - if (entity instanceof LivingEntity le) { - le.hurt(le.damageSources().generic(), (float) amount); - } + LivingEntity le = asLiving(); + if (le != null) le.hurt(le.damageSources().generic(), (float) amount); } public void heal(double amount) { - if (entity instanceof LivingEntity le) { - le.heal((float) amount); - } + LivingEntity le = asLiving(); + if (le != null) le.heal((float) amount); + } + + private LivingEntity asLiving() { + return entity instanceof LivingEntity le ? le : null; } // ---- Invulnerable (MC extension) ---- @@ -188,19 +191,8 @@ public void clearFire() { // ---- Look at (MC extension) ---- - public void lookAt(double x, double y, double z) { - double dx = x - entity.getX(); - double dy = y - entity.getEyeY(); - double dz = z - entity.getZ(); - double horizontalDist = Math.sqrt(dx * dx + dz * dz); - float yaw = (float) (Math.toDegrees(Math.atan2(dz, dx)) - 90.0); - float pitch = (float) (-Math.toDegrees(Math.atan2(dy, horizontalDist))); - entity.setYRot(yaw); - entity.setXRot(pitch); - } - public void lookAt(GameVector3 pos) { - lookAt(pos.x, pos.y, pos.z); - } + 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) ---- @@ -251,10 +243,9 @@ public void addEffect(String effectId, int duration, int amplifier) { } public void addEffect(String effectId, int duration, int amplifier, boolean hideParticles) { - if (!(entity instanceof LivingEntity le)) return; - ResourceLocation rl = ResourceLocation.tryParse(effectId); - if (rl == null) return; - Holder effect = BuiltInRegistries.MOB_EFFECT.getHolder(rl).orElse(null); + 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)); } @@ -265,9 +256,7 @@ public void setEquipment(String slot, String itemId) { if (!(entity instanceof Mob mob)) return; EquipmentSlot equipmentSlot = parseEquipmentSlot(slot); if (equipmentSlot == null) return; - ResourceLocation rl = ResourceLocation.tryParse(itemId); - if (rl == null) return; - Item item = BuiltInRegistries.ITEM.getOptional(rl).orElse(null); + Item item = Box3ScriptUtils.lookupItem(itemId); if (item == null) return; mob.setItemSlot(equipmentSlot, new ItemStack(item)); } @@ -296,21 +285,19 @@ public void setPersistent(boolean v) { // ---- Attributes (MC extension) ---- public double getAttribute(String attributeId) { - if (!(entity instanceof LivingEntity le)) return 0; - ResourceLocation rl = ResourceLocation.tryParse(attributeId); - if (rl == null) return 0; - var holder = BuiltInRegistries.ATTRIBUTE.getHolder(rl); - if (holder.isPresent()) return le.getAttributeValue(holder.get()); + 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) { - if (!(entity instanceof LivingEntity le)) return; - ResourceLocation rl = ResourceLocation.tryParse(attributeId); - if (rl == null) return; - var holder = BuiltInRegistries.ATTRIBUTE.getHolder(rl); - if (holder.isPresent()) { - var instance = le.getAttribute(holder.get()); + 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); } } 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 index 8a487329..57f726b0 100644 --- 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 @@ -22,6 +22,7 @@ import org.mozilla.javascript.ScriptableObject; import java.util.Map; +import java.util.function.Consumer; public class Box3JSPlayer { @@ -88,14 +89,12 @@ public String getWalkState() { public boolean getCanFly() { return player.getAbilities().mayfly; } public void setCanFly(boolean v) { - player.getAbilities().mayfly = v; - player.onUpdateAbilities(); + updateAbility(a -> a.mayfly = v); } public boolean getFlying() { return player.getAbilities().flying; } public void setFlying(boolean v) { - player.getAbilities().flying = v; - player.onUpdateAbilities(); + updateAbility(a -> a.flying = v); } public boolean getCollision() { @@ -115,8 +114,7 @@ public void setCollision(boolean enabled) { public double getFlySpeed() { return player.getAbilities().getFlyingSpeed(); } public void setFlySpeed(double v) { - player.getAbilities().setFlyingSpeed((float) v); - player.onUpdateAbilities(); + updateAbility(a -> a.setFlyingSpeed((float) v)); } // ---- Game Mode ---- @@ -297,17 +295,8 @@ public void setPlayerListName(String name) { // ---- Look at (MC extension) ---- - public void lookAt(double x, double y, double z) { - double dx = x - player.getX(); - double dy = y - player.getEyeY(); - double dz = z - player.getZ(); - double hd = Math.sqrt(dx * dx + dz * dz); - player.setYRot((float) (Math.toDegrees(Math.atan2(dz, dx)) - 90.0)); - player.setXRot((float) (-Math.toDegrees(Math.atan2(dy, hd)))); - } - public void lookAt(GameVector3 pos) { - lookAt(pos.x, pos.y, pos.z); - } + 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 ---- @@ -390,11 +379,9 @@ public void addEffect(String effectId, int duration, int amplifier) { } public void addEffect(String effectId, int duration, int amplifier, boolean hideParticles) { - ResourceLocation rl = ResourceLocation.tryParse(effectId); - if (rl == null) return; - var effect = BuiltInRegistries.MOB_EFFECT.getHolder(rl); - if (effect.isPresent()) { - player.addEffect(new MobEffectInstance(effect.get(), duration, amplifier, false, !hideParticles, true)); + var effect = Box3ScriptUtils.lookupMobEffect(effectId); + if (effect != null) { + player.addEffect(new MobEffectInstance(effect, duration, amplifier, false, !hideParticles, true)); } } @@ -405,11 +392,9 @@ public void clearEffects() { // ---- Sound ---- public void playSound(String path, double volume, double pitch) { - ResourceLocation rl = ResourceLocation.tryParse(path); - if (rl == null) return; - var sound = net.minecraft.core.registries.BuiltInRegistries.SOUND_EVENT.getOptional(rl); - if (sound.isPresent()) { - player.playNotifySound(sound.get(), net.minecraft.sounds.SoundSource.PLAYERS, (float) volume, (float) pitch); + var sound = Box3ScriptUtils.lookupSoundEvent(path); + if (sound != null) { + player.playNotifySound(sound.value(), net.minecraft.sounds.SoundSource.PLAYERS, (float) volume, (float) pitch); } } @@ -429,12 +414,15 @@ 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) { - ResourceLocation rl = ResourceLocation.tryParse(itemId); - if (rl == null) return null; - var item = BuiltInRegistries.ITEM.getOptional(rl); - if (item.isEmpty()) return null; - ItemStack stack = new ItemStack(item.get(), Math.max(1, Math.min(count, 64))); + 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()) { 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..41fbf888 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSScoreboard.java @@ -0,0 +1,95 @@ +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.ArrayList; +import java.util.List; + +class Box3JSScoreboard { + + private final MinecraftServer server; + + Box3JSScoreboard(MinecraftServer server) { + this.server = server; + } + + void addScoreboard(String name) { addScoreboard(name, "dummy"); } + + void addScoreboard(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); + } + + void removeScoreboard(String name) { + Scoreboard sb = server.getScoreboard(); + Objective obj = sb.getObjective(name); + if (obj != null) sb.removeObjective(obj); + } + + 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; + } + + 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 index 6451b6b1..c340f82a 100644 --- 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 @@ -9,6 +9,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; public class Box3JSStorage { @@ -17,6 +18,7 @@ public class Box3JSStorage { 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"); @@ -26,20 +28,24 @@ public Box3JSStorage(Path configDir, Box3ScriptEngine engine) { // ---- GameStorage ---- - /** storage.key — always empty for MC local storage */ public String getKey() { return ""; } - /** storage.getDataStorage(name): GameDataStorage */ public GameDataStorage getDataStorage(String name) { - return new GameDataStorage(name); + return new GameDataStorage(resolveName(name)); } - /** storage.getGroupStorage(name): GameDataStorage — same as getDataStorage in MC */ + /** Shared storage accessible by all projects. */ public GameDataStorage getGroupStorage(String name) { - return new GameDataStorage(name); + return new GameDataStorage("__shared__/" + name); } - // ---- ValueEntry (internal metadata container) ---- + /** 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; @@ -55,7 +61,7 @@ private static class ValueEntry { } } - // ---- ReturnValue (JS-accessible) ---- + // ---- ReturnValue ---- public static class ReturnValue { public String key; @@ -73,7 +79,7 @@ public static class ReturnValue { } } - // ---- QueryList (JS-accessible) ---- + // ---- QueryList ---- public static class QueryList { public boolean isLastPage; @@ -91,8 +97,7 @@ public static class QueryList { public ReturnValue[] getCurrentPage() { int end = Math.min(cursor + pageSize, all.size()); if (cursor >= all.size()) return new ReturnValue[0]; - List slice = all.subList(cursor, end); - return slice.toArray(new ReturnValue[0]); + return all.subList(cursor, end).toArray(new ReturnValue[0]); } public void nextPage() { @@ -107,6 +112,7 @@ public class GameDataStorage { private final String name; private final Path path; + private final Map data; GameDataStorage(String name) { this.name = name; @@ -119,102 +125,99 @@ public class GameDataStorage { 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_.\\-]", "_"); } - /** GameDataStorage.key — read-only space name */ public String getKey() { return name; } - // ---- Internal read/write ---- + // ---- Persist ---- - private synchronized Map read() { - if (!Files.exists(path)) return new LinkedHashMap<>(); - try { - String json = Files.readString(path); - Map map = GSON.fromJson(json, MAP_TYPE); - return map != null ? new LinkedHashMap<>(map) : new LinkedHashMap<>(); - } catch (IOException e) { - return new LinkedHashMap<>(); - } - } - - private synchronized void write(Map map) { + private void persist() { try { Files.createDirectories(path.getParent()); - Files.writeString(path, GSON.toJson(map)); + Files.writeString(path, GSON.toJson(data)); } catch (IOException ignored) {} } // ---- Public API ---- - /** set(key: string, value: JSONValue): void */ public void set(String key, Object value) { if (key == null) return; - Map map = read(); - ValueEntry existing = map.get(key); long now = System.currentTimeMillis(); - if (existing != null) { - existing.value = value; - existing.updateTime = now; - existing.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); - } else { - map.put(key, new ValueEntry(value, now)); + 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(); } - write(map); } - /** get(key: string): value — returns the stored value directly */ public Object get(String key) { if (key == null) return null; - Map map = read(); - ValueEntry entry = map.get(key); - return entry != null ? entry.value : null; + synchronized (data) { + ValueEntry entry = data.get(key); + return entry != null ? entry.value : null; + } } - /** keys(): string[] — returns all keys in this storage */ public String[] keys() { - return read().keySet().toArray(new String[0]); + synchronized (data) { + return data.keySet().toArray(new String[0]); + } } - /** update(key: string, handler: function(prevValue): value): void */ public void update(String key, Function handler) { if (key == null || handler == null) return; - Map map = read(); - ValueEntry entry = map.get(key); - if (entry == null) return; // can't update non-existent key per Box3 spec - Object newValue = engine.callFunction(handler, entry.value); - long now = System.currentTimeMillis(); - entry.value = newValue; - entry.updateTime = now; - entry.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); - write(map); + 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(); + } } - /** remove(key: string): value — returns the old value */ public Object remove(String key) { if (key == null) return null; - Map map = read(); - ValueEntry entry = map.remove(key); - if (entry != null) { - write(map); - return entry.value; + synchronized (data) { + ValueEntry entry = data.remove(key); + if (entry != null) { + persist(); + return entry.value; + } } return null; } - /** increment(key: string, value?: number): number — atomic increment, default delta=1 */ public double increment(String key, double value) { if (key == null) return 0; - // Rhino calls increment(key) with undefined for the second arg, - // which maps to Double.NaN in Java. Handle that case. double delta = Double.isNaN(value) ? 1.0 : value; - synchronized (this) { - Map map = read(); - ValueEntry entry = map.get(key); - long now = System.currentTimeMillis(); + 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; @@ -225,28 +228,26 @@ public double increment(String key, double value) { entry.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); } else { entry = new ValueEntry(delta, now); - map.put(key, entry); + data.put(key, entry); } - write(map); + persist(); return ((Number) entry.value).doubleValue(); } } - // Overload for Rhino: when called with 1 arg public double increment(String key) { return increment(key, 1.0); } - /** list(options: {cursor, pageSize?, ascending?, max?, min?, constraintTarget?}): QueryList */ public QueryList list(Map options) { - Map map = read(); - List results = new ArrayList<>(); - - for (Map.Entry e : map.entrySet()) { - results.add(new ReturnValue(e.getKey(), e.getValue())); + List results; + synchronized (data) { + results = new ArrayList<>(); + for (Map.Entry e : data.entrySet()) { + results.add(new ReturnValue(e.getKey(), e.getValue())); + } } - // Parse options int cursor = 0; int pageSize = 100; boolean ascending = false; @@ -271,7 +272,6 @@ public QueryList list(Map options) { final String target = constraintTarget; final boolean asc = ascending; - // Sort if (doSort) { results.sort((a, b) -> { double va = extractSortValue(a.value, target); @@ -281,7 +281,6 @@ public QueryList list(Map options) { }); } - // Filter by min/max if (max != null || min != null) { List filtered = new ArrayList<>(); for (ReturnValue rv : results) { @@ -301,7 +300,6 @@ private double extractSortValue(Object value, String target) { if (value instanceof Number n) return n.doubleValue(); return 0; } - // Navigate nested path like "a.b.c" Object current = value; for (String part : target.split("\\.")) { if (current instanceof Map m) { @@ -314,9 +312,9 @@ private double extractSortValue(Object value, String target) { return 0; } - /** destroy(): void — delete this data storage space */ public void destroy() { - synchronized (this) { + 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..f02e7aea --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSTeam.java @@ -0,0 +1,55 @@ +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; + +class Box3JSTeam { + + private final MinecraftServer server; + + Box3JSTeam(MinecraftServer server) { + this.server = server; + } + + void createTeam(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)); + } + } + + void removeTeam(String name) { + Scoreboard sb = server.getScoreboard(); + PlayerTeam team = sb.getPlayerTeam(name); + if (team != null) sb.removePlayerTeam(team); + } + + 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; + } +} 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 index b29e9f13..e305c1f4 100644 --- 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 @@ -82,13 +82,10 @@ public int id(String name) { Integer id = nameToId.get(name); if (id != null) return id; // Try vanilla block by ResourceLocation - ResourceLocation rl = ResourceLocation.tryParse(name); - if (rl != null) { - Block block = BuiltInRegistries.BLOCK.getOptional(rl).orElse(null); - if (block != null && block != Blocks.AIR) { - Integer foundId = blockToId.get(block); - if (foundId != null) return foundId; - } + Block block = Box3ScriptUtils.lookupBlock(name); + if (block != null && block != Blocks.AIR) { + Integer foundId = blockToId.get(block); + if (foundId != null) return foundId; } return 0; } @@ -264,12 +261,10 @@ public void setSpawner(int x, int y, int z, String entityType) { var be = level.getBlockEntity(pos); if (!(be instanceof net.minecraft.world.level.block.entity.SpawnerBlockEntity spawnerBe)) return; - ResourceLocation rl = ResourceLocation.tryParse(entityType); - if (rl == null) return; - var opt = BuiltInRegistries.ENTITY_TYPE.getOptional(rl); - if (opt.isEmpty()) return; + var opt = Box3ScriptUtils.lookupEntityType(entityType); + if (opt == null) return; - spawnerBe.setEntityId(opt.get(), level.getRandom()); + spawnerBe.setEntityId(opt, level.getRandom()); } public void setSpawner(GameVector3 pos, String entityType) { setSpawner((int) pos.x, (int) pos.y, (int) pos.z, entityType); @@ -318,9 +313,7 @@ private Block resolveBlock(Object voxel) { Block b = resourceToBlock.get(s.toLowerCase(Locale.ROOT)); if (b != null) return b; // Try vanilla block by ResourceLocation (e.g. "minecraft:stone" or "stone") - ResourceLocation rl = ResourceLocation.tryParse(s); - if (rl == null) return null; - return BuiltInRegistries.BLOCK.getOptional(rl).orElse(null); + return Box3ScriptUtils.lookupBlock(s); } return null; } 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 index 706a16bf..aeeadd10 100644 --- 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 @@ -1,34 +1,16 @@ package com.box3lab.box3js.script; -import org.mozilla.javascript.NativeObject; -import org.mozilla.javascript.ScriptableObject; - import net.minecraft.commands.CommandSourceStack; import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; import net.minecraft.core.Holder; +import net.minecraft.core.component.DataComponents; import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.core.particles.ParticleOptions; -import net.minecraft.network.chat.Component; 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.server.level.ServerPlayer; import net.minecraft.sounds.SoundSource; import net.minecraft.world.Difficulty; -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 net.minecraft.world.scores.PlayerTeam; -import net.minecraft.ChatFormatting; -import net.minecraft.server.level.ServerBossEvent; -import net.minecraft.world.BossEvent.BossBarColor; -import net.minecraft.world.BossEvent.BossBarOverlay; -import net.minecraft.core.component.DataComponents; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LightningBolt; @@ -37,17 +19,11 @@ 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.border.WorldBorder; -import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.GameRules; import net.minecraft.world.level.Level; import net.minecraft.world.level.biome.Biome; -import net.minecraft.world.phys.shapes.CollisionContext; -import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.border.WorldBorder; import net.minecraft.world.level.storage.ServerLevelData; -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 org.mozilla.javascript.Function; import java.util.*; @@ -57,11 +33,18 @@ public class Box3JSWorld { private final MinecraftServer server; private final Box3ScriptEngine engine; private String projectName; - private final Map bossBars = new HashMap<>(); + 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; } @@ -72,16 +55,10 @@ public Box3JSWorld(MinecraftServer server, Box3ScriptEngine engine) { public int currentTick() { return server.getTickCount(); } - public double getRainDensity() { - return server.overworld().getRainLevel(1.0f); - } - public void setRainDensity(double v) { - server.overworld().getLevelData().setRaining(v > 0); - } + public double getRainDensity() { return server.overworld().getRainLevel(1.0f); } + public void setRainDensity(double v) { server.overworld().getLevelData().setRaining(v > 0); } - public double getThunderDensity() { - return server.overworld().getThunderLevel(1.0f); - } + public double getThunderDensity() { return server.overworld().getThunderLevel(1.0f); } public void setThunderDensity(double v) { ((ServerLevelData) server.overworld().getLevelData()).setThundering(v > 0); } @@ -106,59 +83,41 @@ public void setTimeScale(double v) { // ---- Difficulty ---- - public String getDifficulty() { - return server.overworld().getDifficulty().getKey(); - } + public String getDifficulty() { return server.overworld().getDifficulty().getKey(); } public void setDifficulty(Object v) { - Difficulty diff; - if (v instanceof Number n) { - diff = Difficulty.byId(n.intValue()); - } else { - diff = Difficulty.byName(v.toString()); - } + Difficulty diff = v instanceof Number n ? Difficulty.byId(n.intValue()) : Difficulty.byName(v.toString()); if (diff != null) server.setDifficulty(diff, true); } - // ---- Game Rules (MC extension) ---- + // ---- Game Rules ---- public Object getGameRule(String name) { GameRules rules = server.overworld().getGameRules(); - switch (name) { - case "doDaylightCycle": return rules.getBoolean(GameRules.RULE_DAYLIGHT); - case "doWeatherCycle": return rules.getBoolean(GameRules.RULE_WEATHER_CYCLE); - case "keepInventory": return rules.getBoolean(GameRules.RULE_KEEPINVENTORY); - case "doMobSpawning": return rules.getBoolean(GameRules.RULE_DOMOBSPAWNING); - case "doFireTick": return rules.getBoolean(GameRules.RULE_DOFIRETICK); - case "mobGriefing": return rules.getBoolean(GameRules.RULE_MOBGRIEFING); - case "doImmediateRespawn": return rules.getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN); - default: return null; - } + 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) { GameRules rules = server.overworld().getGameRules(); switch (name) { - case "doDaylightCycle": - rules.getRule(GameRules.RULE_DAYLIGHT).set(coerceBool(value), server); break; - case "doWeatherCycle": - rules.getRule(GameRules.RULE_WEATHER_CYCLE).set(coerceBool(value), server); break; - case "keepInventory": - rules.getRule(GameRules.RULE_KEEPINVENTORY).set(coerceBool(value), server); break; - case "doMobSpawning": - rules.getRule(GameRules.RULE_DOMOBSPAWNING).set(coerceBool(value), server); break; - case "doFireTick": - rules.getRule(GameRules.RULE_DOFIRETICK).set(coerceBool(value), server); break; - case "mobGriefing": - rules.getRule(GameRules.RULE_MOBGRIEFING).set(coerceBool(value), server); break; - case "doImmediateRespawn": - rules.getRule(GameRules.RULE_DO_IMMEDIATE_RESPAWN).set(coerceBool(value), server); break; + 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; } } - private static boolean coerceBool(Object v) { - return v instanceof Boolean b ? b : Boolean.parseBoolean(v.toString()); - } - // ---- Spawn ---- public GameVector3 getSpawnPoint() { @@ -172,11 +131,9 @@ public void setWorldSpawn(GameVector3 pos) { // ---- Entity spawning ---- public Box3JSEntity spawnEntity(String type, GameVector3 pos) { - ResourceLocation rl = ResourceLocation.tryParse(type); - if (rl == null) return null; - var opt = BuiltInRegistries.ENTITY_TYPE.getOptional(rl); - if (opt.isEmpty()) return null; - Entity entity = opt.get().create(server.overworld()); + 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); @@ -188,80 +145,63 @@ public Box3JSEntity spawnEntity(String type, GameVector3 pos) { 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)); + 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) { @@ -271,204 +211,51 @@ public void onMessage(Function handler) { // ---- Entity Query ---- - public 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; - } - - public Box3JSEntity querySelector(String selector) { - List all = querySelectorAll(selector); - return all.isEmpty() ? null : all.get(0); - } - - private boolean matchesSelector(Box3JSEntity entity, String selector) { - if (selector.equals("*") || selector.equals("player")) return entity.isPlayer(); - if (selector.startsWith("#")) { - String id = selector.substring(1); - return id.equals(entity.getId()); - } - if (selector.startsWith(".")) { - String tag = selector.substring(1); - return entity.hasTag(tag); - } - return false; - } + 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); + 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); - } + 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) { - CommandSourceStack source = server.createCommandSourceStack(); - server.getCommands().performPrefixedCommand(source, cmd); + server.getCommands().performPrefixedCommand(server.createCommandSourceStack(), cmd); } // ---- Scoreboard ---- - public void addScoreboard(String name) { addScoreboard(name, "dummy"); } - public void addScoreboard(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); - } - public void removeScoreboard(String name) { - Scoreboard sb = server.getScoreboard(); - Objective obj = sb.getObjective(name); - if (obj != null) sb.removeObjective(obj); - } - public void setScore(Object entityOrName, String objectiveName, int value) { - Scoreboard sb = server.getScoreboard(); - Objective obj = sb.getObjective(objectiveName); - if (obj == null) return; - String name = resolveScoreName(entityOrName); - if (name == null) return; - sb.getOrCreatePlayerScore(ScoreHolder.forNameOnly(name), obj).set(value); - } - public int getScore(Object entityOrName, String objectiveName) { - Scoreboard sb = server.getScoreboard(); - Objective obj = sb.getObjective(objectiveName); - if (obj == null) return 0; - String name = resolveScoreName(entityOrName); - if (name == null) return 0; - ScoreAccess access = sb.getOrCreatePlayerScore(ScoreHolder.forNameOnly(name), obj); - return access.get(); - } - public void showScoreboard(String slot, String objectiveName) { - Scoreboard sb = server.getScoreboard(); - DisplaySlot displaySlot = switch (slot.toLowerCase()) { - case "list" -> DisplaySlot.LIST; - case "belowname", "below_name" -> DisplaySlot.BELOW_NAME; - default -> DisplaySlot.SIDEBAR; - }; - Objective obj = sb.getObjective(objectiveName); - sb.setDisplayObjective(displaySlot, obj); - } - public void hideScoreboard(String slot) { - Scoreboard sb = server.getScoreboard(); - DisplaySlot displaySlot = switch (slot.toLowerCase()) { - case "list" -> DisplaySlot.LIST; - case "belowname", "below_name" -> DisplaySlot.BELOW_NAME; - default -> DisplaySlot.SIDEBAR; - }; - sb.setDisplayObjective(displaySlot, null); - } - public java.util.List listScores(String objectiveName) { - java.util.List result = new ArrayList<>(); - Scoreboard sb = server.getScoreboard(); - Objective obj = sb.getObjective(objectiveName); - if (obj == null) return result; - for (ServerPlayer player : server.getPlayerList().getPlayers()) { - int s = sb.getOrCreatePlayerScore(ScoreHolder.forNameOnly(player.getScoreboardName()), obj).get(); - NativeObject m = new NativeObject(); - ScriptableObject.putProperty(m, "name", player.getScoreboardName()); - ScriptableObject.putProperty(m, "value", s); - result.add(m); - } - return result; - } + public void addScoreboard(String name) { scoreboard.addScoreboard(name); } + public void addScoreboard(String name, String criteria) { scoreboard.addScoreboard(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) { - ServerBossEvent bar = bossBars.get(name); - if (bar == null) { - BossBarColor color = colorName == null ? BossBarColor.WHITE : 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; - }; - bar = new ServerBossEvent(Component.literal(text), color, BossBarOverlay.PROGRESS); - bossBars.put(name, bar); - } else { - bar.setName(Component.literal(text)); - if (colorName != null) bar.setColor(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; - }); - } - bar.setProgress((float) Math.max(0, Math.min(1, progress))); - for (ServerPlayer sp : server.getPlayerList().getPlayers()) bar.addPlayer(sp); - } - public void removeBossbar(String name) { - ServerBossEvent bar = bossBars.remove(name); - if (bar != null) bar.removeAllPlayers(); - } + public void showBossbar(String name, String text, double progress, String colorName) { bossbar.showBossbar(name, text, progress, colorName); } + public void removeBossbar(String name) { bossbar.removeBossbar(name); } // ---- Team ---- - public void createTeam(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)); - } - } - public void removeTeam(String name) { - Scoreboard sb = server.getScoreboard(); - PlayerTeam team = sb.getPlayerTeam(name); - if (team != null) sb.removePlayerTeam(team); - } - public void joinTeam(Object entityOrName, String teamName) { - Scoreboard sb = server.getScoreboard(); - PlayerTeam team = sb.getPlayerTeam(teamName); - if (team == null) return; - String name = resolveScoreName(entityOrName); - if (name != null) sb.addPlayerToTeam(name, team); - } - public void leaveTeam(Object entityOrName) { - Scoreboard sb = server.getScoreboard(); - String name = resolveScoreName(entityOrName); - if (name != null) sb.removePlayerFromTeam(name); - } - public String getTeamOf(Object entityOrName) { - Scoreboard sb = server.getScoreboard(); - String name = resolveScoreName(entityOrName); - if (name == null) return null; - PlayerTeam team = sb.getPlayersTeam(name); - return team != null ? team.getName() : null; - } + public void createTeam(String name, String colorName) { team.createTeam(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 ---- @@ -485,44 +272,34 @@ public void shrinkBorder(double targetSize, double seconds) { // ---- Lightning ---- public boolean strikeLightning(double x, double y, double z) { - ServerLevel level = server.overworld(); - LightningBolt bolt = EntityType.LIGHTNING_BOLT.create(level); + LightningBolt bolt = EntityType.LIGHTNING_BOLT.create(server.overworld()); if (bolt == null) return false; bolt.moveTo(x, y, z); bolt.setVisualOnly(false); - level.addFreshEntity(bolt); + server.overworld().addFreshEntity(bolt); return true; } - public boolean strikeLightning(GameVector3 pos) { - return strikeLightning(pos.x, pos.y, pos.z); - } + public boolean strikeLightning(GameVector3 pos) { return strikeLightning(pos.x, pos.y, pos.z); } public boolean strikeLightning(double x, double y, double z, double damage) { - ServerLevel level = server.overworld(); - LightningBolt bolt = EntityType.LIGHTNING_BOLT.create(level); + 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); - level.addFreshEntity(bolt); + server.overworld().addFreshEntity(bolt); return true; } - public boolean strikeLightning(GameVector3 pos, double damage) { - return strikeLightning(pos.x, pos.y, pos.z, damage); - } + public boolean strikeLightning(GameVector3 pos, double damage) { return strikeLightning(pos.x, pos.y, pos.z, damage); } - // ---- Projectile (MC extension) ---- + // ---- Projectile ---- public Box3JSEntity launchProjectile(String type, double x, double y, double z, double tx, double ty, double tz, double speed) { - ServerLevel level = server.overworld(); - ResourceLocation rl = ResourceLocation.tryParse(type); - if (rl == null) return null; - var opt = BuiltInRegistries.ENTITY_TYPE.getOptional(rl); - if (opt.isEmpty()) return null; - Entity entity = opt.get().create(level); + 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) { @@ -532,8 +309,7 @@ public Box3JSEntity launchProjectile(String type, double x, double y, double z, if (entity instanceof net.minecraft.world.entity.projectile.Projectile proj) { proj.shoot(dx, dy, dz, (float) speed, 0); } - - level.addFreshEntity(entity); + server.overworld().addFreshEntity(entity); return new Box3JSEntity(entity, server, engine); } public Box3JSEntity launchProjectile(String type, GameVector3 pos, GameVector3 target, double speed) { @@ -543,8 +319,7 @@ public Box3JSEntity launchProjectile(String type, GameVector3 pos, GameVector3 t // ---- Firework ---- public void launchFirework(double x, double y, double z, String color, String shape) { - ServerLevel level = server.overworld(); - int colorInt = switch (color != null ? color.toLowerCase(Locale.ROOT) : "") { + int colorInt = switch (color != null ? color.toLowerCase(java.util.Locale.ROOT) : "") { case "red" -> 0xFF0000; case "blue" -> 0x0000FF; case "green", "lime" -> 0x00FF00; @@ -570,8 +345,8 @@ public void launchFirework(double x, double y, double z, String color, String sh 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(level, x, y, z, rocket); - level.addFreshEntity(entity); + 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); @@ -580,49 +355,31 @@ public void launchFirework(GameVector3 pos, String color, String 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 = resolveParticle(type); - if (particle != null) { - server.overworld().sendParticles(particle, x, y, z, count, dx, dy, dz, 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 = resolveParticle(type); + var particle = Box3ScriptUtils.lookupParticle(type); if (particle == null) return; - ServerLevel level = server.overworld(); for (int i = 0; i < count; i++) { double angle = (2.0 * Math.PI * i) / count; - double px = x + Math.cos(angle) * radius; - double pz = z + Math.sin(angle) * radius; - level.sendParticles(particle, px, y, pz, 1, 0, 0, 0, 0); + 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); } - private ParticleOptions resolveParticle(String type) { - ResourceLocation rl = ResourceLocation.tryParse(type); - if (rl == null) return null; - var particle = BuiltInRegistries.PARTICLE_TYPE.getOptional(rl); - if (particle.isEmpty()) return null; - var p = particle.get(); - if (p instanceof ParticleOptions options) return options; - return null; - } // ---- Drop Item ---- public void dropItem(double x, double y, double z, String itemId, int count) { - ServerLevel level = server.overworld(); - ResourceLocation rl = ResourceLocation.tryParse(itemId); - if (rl == null) return; - var item = BuiltInRegistries.ITEM.getOptional(rl).orElse(null); + var item = Box3ScriptUtils.lookupItem(itemId); if (item == null) return; ItemStack stack = new ItemStack(item, Math.max(1, count)); - ItemEntity itemEntity = new ItemEntity(level, x, y, z, stack); - level.addFreshEntity(itemEntity); + 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); @@ -630,127 +387,30 @@ public void dropItem(GameVector3 pos, String itemId, int count) { // ---- Query ---- - public Object raycast(GameVector3 origin, GameVector3 direction) { - return raycast(origin, direction, 5.0); - } - - public 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, 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, e -> 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; - } - - public 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, e -> true)) { - result.add(new Box3JSEntity(e, server, engine)); - } - return result; - } - - public 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, e -> true)) { - result.add(new Box3JSEntity(e, server, engine)); - } - return result; - } - public List entitiesInRadius(GameVector3 pos, double radius) { - return entitiesInRadius(pos.x, pos.y, pos.z, radius); - } - - public 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"); - } - public String getBiome(GameVector3 pos) { - return getBiome((int) pos.x, (int) pos.y, (int) pos.z); - } + 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) { 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); - } + 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) { - ResourceLocation rl = ResourceLocation.tryParse(path); - if (rl == null) return; - var sound = BuiltInRegistries.SOUND_EVENT.getHolder(rl); - if (sound.isEmpty()) return; - var packet = new ClientboundSoundPacket(sound.get(), SoundSource.PLAYERS, x, y, z, (float) volume, (float) pitch, server.overworld().getRandom().nextLong()); - for (ServerPlayer sp : server.getPlayerList().getPlayers()) { - sp.connection.send(packet); - } + 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); @@ -761,95 +421,4 @@ public void playSound(String path, GameVector3 pos, double volume, double pitch) public void sendMessage(String target, Object data) { engine.fireMessage(engine.getCurrentProject(), target, data); } - - // ---- Helpers ---- - - private 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; - } - - // ---- Callback interfaces ---- - - @FunctionalInterface - public interface PlayerJoinCallback { - void onJoin(Box3JSEntity entity); - } - - @FunctionalInterface - public interface PlayerLeaveCallback { - void onLeave(Box3JSEntity entity); - } - - @FunctionalInterface - public interface VoxelDestroyCallback { - void onDestroy(Box3JSEntity entity, int x, int y, int z, String voxel, long tick); - } - - @FunctionalInterface - public interface VoxelContactCallback { - void onContact(Box3JSEntity entity, int voxel, int x, int y, int z, int axis, double force, long tick); - } - - @FunctionalInterface - public interface InteractCallback { - void onInteract(Box3JSEntity entity, Box3JSEntity target, long tick); - } - - @FunctionalInterface - public interface ChatCallback { - void onChat(Box3JSEntity entity, String message, long tick); - } - - @FunctionalInterface - public interface FluidEnterCallback { - void onEnter(Box3JSEntity entity, String fluid, int x, int y, int z, long tick); - } - - @FunctionalInterface - public interface FluidLeaveCallback { - void onLeave(Box3JSEntity entity, String fluid, int x, int y, int z, long tick); - } - - @FunctionalInterface - public interface EntityContactCallback { - void onContact(Box3JSEntity entity, Box3JSEntity other, long tick); - } - - @FunctionalInterface - public interface EntitySeparateCallback { - void onSeparate(Box3JSEntity entity, Box3JSEntity other, long tick); - } - - @FunctionalInterface - public interface BlockPlaceCallback { - void onPlace(Box3JSEntity entity, int x, int y, int z, String voxel, int voxelId, long tick); - } - - @FunctionalInterface - public interface EntityDeathCallback { - void onDeath(Box3JSEntity entity, Box3JSEntity killer, long tick); - } - - @FunctionalInterface - public interface PlayerRespawnCallback { - void onRespawn(Box3JSEntity entity); - } - - @FunctionalInterface - public interface BlockActivateCallback { - void onActivate(Box3JSEntity entity, int x, int y, int z, String voxel, long tick); - } - - @FunctionalInterface - public interface EntityDamageCallback { - void onDamage(Box3JSEntity entity, double amount, String source, Box3JSEntity attacker, long tick); - } - - @FunctionalInterface - public interface MessageCallback { - void onMessage(String from, Object 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 index 54d1ac72..4668c1ea 100644 --- 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 @@ -1,193 +1,241 @@ package com.box3lab.box3js.script; import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +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.minecraft.server.MinecraftServer; import net.neoforged.neoforge.event.RegisterCommandsEvent; -import java.io.IOException; -import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; +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) { - var dispatcher = event.getDispatcher(); - - dispatcher.register( - literal("box3script") - .requires(src -> src.hasPermission(2)) - // --- create --- - .then(literal("create") - .then(argument("name", StringArgumentType.word()) - .executes(ctx -> { - String name = StringArgumentType.getString(ctx, "name"); - Path projectDir = resolve(name, ctx.getSource().getServer()); - if (Files.exists(projectDir)) { - ctx.getSource().sendFailure( - Component.literal("Project already exists: " + name)); - return 0; - } - try { - copyTemplate(projectDir, name); - String absPath = projectDir.toAbsolutePath().toString(); - Component msg = Component.literal("Project created: " + name + "\n") - .append(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"))))) - .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 (IOException e) { - ctx.getSource().sendFailure( - Component.literal("Failed to create: " + e.getMessage())); - } - return 1; - }))) - // --- stop --- - .then(literal("stop") - .executes(ctx -> { - Box3ScriptEngine.get().reset(); - ctx.getSource().sendSuccess( - () -> Component.literal("All scripts stopped."), - false); - return 1; - }) - .then(argument("project", StringArgumentType.word()) - .executes(ctx -> { - String project = StringArgumentType.getString(ctx, "project"); - Box3ScriptConfig.get().setEnabled(project, false); - var server = ctx.getSource().getServer(); - Box3ScriptEngine.get().reset(); - Box3ScriptEngine.get().autoLoad(server); - ctx.getSource().sendSuccess( - () -> Component.literal("Stopped and disabled: " + project), - false); - return 1; - }))) - // --- list --- - .then(literal("list") - .executes(ctx -> { - var server = ctx.getSource().getServer(); - var config = Box3ScriptConfig.get(); - config.discover(server); - var projects = config.listProjects(); - if (projects.isEmpty()) { - ctx.getSource().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]"; - sb.append(" ").append(status).append(" §f").append(name).append("\n"); - }); - String output = sb.toString().trim(); - ctx.getSource().sendSuccess( - () -> Component.literal(output), - false); - } - return 1; - })) - // --- on --- - .then(literal("on") - .then(argument("project", StringArgumentType.word()) - .executes(ctx -> { - String project = StringArgumentType.getString(ctx, "project"); - Box3ScriptConfig.get().setEnabled(project, true); - ctx.getSource().sendSuccess( - () -> Component.literal("Enabled: " + project), - false); - return 1; - })) - .then(literal("all") - .executes(ctx -> { - Box3ScriptConfig.get().setAllEnabled(true); - ctx.getSource().sendSuccess( - () -> Component.literal("All projects enabled."), - false); - return 1; - }))) - // --- off --- - .then(literal("off") - .then(argument("project", StringArgumentType.word()) - .executes(ctx -> { - String project = StringArgumentType.getString(ctx, "project"); - Box3ScriptConfig.get().setEnabled(project, false); - 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 --- - .then(literal("reload") - .executes(ctx -> { - var server = ctx.getSource().getServer(); - Box3ScriptEngine.get().reset(); - Box3ScriptEngine.get().autoLoad(server); - ctx.getSource().sendSuccess( - () -> Component.literal("Scripts reloaded."), - false); - return 1; - })) + event.getDispatcher().register( + literal("box3script") + .requires(src -> src.hasPermission(2)) + .then(createCommand()) + .then(stopCommand()) + .then(listCommand()) + .then(onCommand()) + .then(offCommand()) + .then(reloadCommand()) + .then(watchCommand()) ); } - private static final String[] TEMPLATE_FILES = { - "gitignore.template", - "package.json", - "tsconfig.json", - "build.mjs", - "src/app.ts", - "types/globals.d.ts", - }; - - /** - * Copies the TypeScript project template from classpath to the target directory. - */ - private static void copyTemplate(Path projectDir, String projectName) throws IOException { - Files.createDirectories(projectDir); - for (String relPath : TEMPLATE_FILES) { - // gitignore.template → .gitignore - 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 = Box3ScriptCommand.class.getResourceAsStream(resourcePath)) { - if (in == null) { - throw new IOException("Template file not found: " + resourcePath); - } - Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING); - } - // Replace placeholders in app.ts - if (relPath.equals("src/app.ts")) { - String content = Files.readString(dest); - content = content.replace("PROJECT_NAME", projectName); - Files.writeString(dest, content); - } + // ---- 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; } } - private static Path resolve(String input, MinecraftServer server) { - Path p = Path.of(input); - if (p.isAbsolute()) return p; - return Box3ScriptConfig.get().getScriptDir(server).resolve(input).normalize(); + // --- 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()) + .executes(ctx -> { + String project = StringArgumentType.getString(ctx, "project"); + Box3ScriptConfig.get().setEnabled(project, false); + return safeRun(ctx.getSource(), "Stopped: " + project, () -> + Box3ScriptEngine.get().removeProject(project) + ); + })); + } + + // --- list --- + + private static LiteralArgumentBuilder listCommand() { + return literal("list") + .executes(ctx -> { + var config = Box3ScriptConfig.get(); + config.discover(ctx.getSource().getServer()); + var projects = config.listProjects(); + if (projects.isEmpty()) { + ctx.getSource().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]"; + sb.append(" ").append(status).append(" §f").append(name).append("\n"); + }); + ctx.getSource().sendSuccess( + () -> Component.literal(sb.toString().trim()), false); + } + return 1; + }); + } + + // --- on --- + + private static LiteralArgumentBuilder onCommand() { + return literal("on") + .then(argument("project", StringArgumentType.word()) + .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()) + .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(); + } + })); + } + + // --- 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; + })); + } + + // --- helpers --- + + 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/Box3ScriptEngine.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java index 26e02999..be80a474 100644 --- 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 @@ -13,12 +13,11 @@ import org.mozilla.javascript.commonjs.module.provider.UrlModuleSourceProvider; import java.io.IOException; -import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; public class Box3ScriptEngine { @@ -31,32 +30,10 @@ public class Box3ScriptEngine { private MinecraftServer server; private boolean initialized; - private final List tickCallbacks = new CopyOnWriteArrayList<>(); - private final List joinCallbacks = new CopyOnWriteArrayList<>(); - private final List leaveCallbacks = new CopyOnWriteArrayList<>(); - private final List voxelDestroyCallbacks = new CopyOnWriteArrayList<>(); - private final List voxelContactCallbacks = new CopyOnWriteArrayList<>(); - private final List interactCallbacks = new CopyOnWriteArrayList<>(); - private final List chatCallbacks = new CopyOnWriteArrayList<>(); - private final List fluidEnterCallbacks = new CopyOnWriteArrayList<>(); - private final List fluidLeaveCallbacks = new CopyOnWriteArrayList<>(); - private final List entityContactCallbacks = new CopyOnWriteArrayList<>(); - private final List entitySeparateCallbacks = new CopyOnWriteArrayList<>(); - private final List blockPlaceCallbacks = new CopyOnWriteArrayList<>(); - private final List entityDeathCallbacks = new CopyOnWriteArrayList<>(); - private final List respawnCallbacks = new CopyOnWriteArrayList<>(); - private final List blockActivateCallbacks = new CopyOnWriteArrayList<>(); - private final List entityDamageCallbacks = new CopyOnWriteArrayList<>(); - private final Map> messageCallbacks = new ConcurrentHashMap<>(); + final Box3JSEventBus bus = new Box3JSEventBus(); private String currentProject; - private final Map voxelContactTracked = new ConcurrentHashMap<>(); - private final Map fluidStateTracked = new ConcurrentHashMap<>(); - private final Set entityContactPairs = ConcurrentHashMap.newKeySet(); - private final Map playerChatHandlers = new ConcurrentHashMap<>(); - private final Map> entityCustomProps = new HashMap<>(); + private Consumer errorReporter; private final Map projectRequires = new HashMap<>(); - private final List timers = new ArrayList<>(); - private int timerIdCounter; public static Box3ScriptEngine get() { return INSTANCE; @@ -115,34 +92,106 @@ public Object eval(String code) { } } - /** Tick callback from Box3JSWorld — wraps to restore project context */ + /** 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; - tickCallbacks.add(() -> { + 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); } - }); - } - public void addJoinCallback(Box3JSWorld.PlayerJoinCallback cb) { joinCallbacks.add(cb); } - public void addLeaveCallback(Box3JSWorld.PlayerLeaveCallback cb) { leaveCallbacks.add(cb); } - public void addVoxelDestroyCallback(Box3JSWorld.VoxelDestroyCallback cb) { voxelDestroyCallbacks.add(cb); } - public void addVoxelContactCallback(Box3JSWorld.VoxelContactCallback cb) { voxelContactCallbacks.add(cb); } - public void addInteractCallback(Box3JSWorld.InteractCallback cb) { interactCallbacks.add(cb); } - public void addChatCallback(Box3JSWorld.ChatCallback cb) { chatCallbacks.add(cb); } - public void addFluidEnterCallback(Box3JSWorld.FluidEnterCallback cb) { fluidEnterCallbacks.add(cb); } - public void addFluidLeaveCallback(Box3JSWorld.FluidLeaveCallback cb) { fluidLeaveCallbacks.add(cb); } - public void addEntityContactCallback(Box3JSWorld.EntityContactCallback cb) { entityContactCallbacks.add(cb); } - public void addEntitySeparateCallback(Box3JSWorld.EntitySeparateCallback cb) { entitySeparateCallbacks.add(cb); } - public void addBlockPlaceCallback(Box3JSWorld.BlockPlaceCallback cb) { blockPlaceCallbacks.add(cb); } - public void addEntityDeathCallback(Box3JSWorld.EntityDeathCallback cb) { entityDeathCallbacks.add(cb); } - public void addRespawnCallback(Box3JSWorld.PlayerRespawnCallback cb) { respawnCallbacks.add(cb); } - public void addBlockActivateCallback(Box3JSWorld.BlockActivateCallback cb) { blockActivateCallbacks.add(cb); } - public void addEntityDamageCallback(Box3JSWorld.EntityDamageCallback cb) { entityDamageCallbacks.add(cb); } - public void addMessageCallback(String project, Box3JSWorld.MessageCallback cb) { - messageCallbacks.computeIfAbsent(project, k -> new CopyOnWriteArrayList<>()).add(cb); - } - public void setPlayerChatHandler(UUID uuid, Function handler) { playerChatHandlers.put(uuid, handler); } + }; + } + + 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; @@ -150,120 +199,119 @@ public void setCurrentProject(String name) { } public String getCurrentProject() { return currentProject; } + // ---- Project lifecycle ---- + + /** Remove one project's callbacks without affecting others. */ + public void removeProject(String project) { + bus.removeProject(project); + projectRequires.remove(project); + Box3JS.LOGGER.info("Removed project: {}", project); + } + + // ---- Message routing ---- + public void fireMessage(String sender, String target, Object data) { if ("*".equals(target)) { - for (var entry : messageCallbacks.entrySet()) { + for (var entry : bus.messageCallbacks.entrySet()) { if (!entry.getKey().equals(sender)) { - for (var cb : entry.getValue()) { - String prev = currentProject; - setCurrentProject(entry.getKey()); - try { cb.onMessage(sender, data); } finally { setCurrentProject(prev); } - } + for (var cb : entry.getValue()) cb.onMessage(sender, data); } } } else { - List cbs = messageCallbacks.get(target); + List cbs = bus.messageCallbacks.get(target); if (cbs != null) { - for (var cb : cbs) { - String prev = currentProject; - setCurrentProject(target); - try { cb.onMessage(sender, data); } finally { setCurrentProject(prev); } - } + for (var cb : cbs) cb.onMessage(sender, data); } } } + // ---- Timers ---- + public int scheduleTimeout(Function handler, int ticks) { - int id = ++timerIdCounter; - timers.add(new TimerEntry(id, handler, ticks, 0, currentProject)); + 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) { - int id = ++timerIdCounter; - timers.add(new TimerEntry(id, handler, ticks, ticks, currentProject)); + 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) { - timers.removeIf(t -> t.id == id); + for (var list : bus.timers.values()) { + if (list.removeIf(t -> t.id == id)) return; + } } private void fireTimers() { - var toFire = new ArrayList(); - var toRemove = new ArrayList(); - for (var t : timers) { - if (--t.remaining <= 0) { - toFire.add(t); - if (t.interval == 0) { - toRemove.add(t); - } else { - t.remaining = t.interval; + 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; } } - } - timers.removeAll(toRemove); - for (var t : toFire) { - String prev = currentProject; - setCurrentProject(t.project); - try { callFunction(t.handler); } finally { setCurrentProject(prev); } + list.removeAll(toRemove); + for (var t : toFire) { + runInContext(t.project, () -> callFunction(t.handler)); + } } } + // ---- Tick ---- + public void fireTick() { fireTimers(); - for (Runnable cb : tickCallbacks) cb.run(); - // Voxel contact tracking: check if any tracked entity changed block position - if (!voxelContactCallbacks.isEmpty()) { + 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 = voxelContactTracked.put(uuid, current); + 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(); - for (var cb : voxelContactCallbacks) { - cb.onContact(entity, voxelId, current.getX(), current.getY(), current.getZ(), 1, force, tick); - } - } - } - } - // Fluid state tracking - if (!fluidEnterCallbacks.isEmpty() || !fluidLeaveCallbacks.isEmpty()) { - long tick = server.getTickCount(); - for (ServerPlayer player : server.getPlayerList().getPlayers()) { - UUID uuid = player.getUUID(); - String current = player.isInLava() ? "lava" : player.isInWater() ? "water" : "none"; - String last = fluidStateTracked.put(uuid, current); - if (!current.equals(last)) { - Box3JSEntity entity = new Box3JSEntity(player, server, this); - BlockPos pos = player.blockPosition(); - if (!"none".equals(current) && !"none".equals(last) && last != null) { - // Switched fluid type (water→lava or lava→water) - for (var cb : fluidLeaveCallbacks) { - cb.onLeave(entity, last, pos.getX(), pos.getY(), pos.getZ(), tick); + runInContext(project, () -> { + for (var cb : callbacks) { + cb.onContact(entity, voxelId, current.getX(), current.getY(), current.getZ(), 1, force, tick); } - for (var cb : fluidEnterCallbacks) { - cb.onEnter(entity, current, pos.getX(), pos.getY(), pos.getZ(), tick); - } - } else if (!"none".equals(current) && ("none".equals(last) || last == null)) { - for (var cb : fluidEnterCallbacks) { - cb.onEnter(entity, current, pos.getX(), pos.getY(), pos.getZ(), tick); - } - } else if ("none".equals(current) && last != null && !"none".equals(last)) { - for (var cb : fluidLeaveCallbacks) { - cb.onLeave(entity, last, pos.getX(), pos.getY(), pos.getZ(), tick); - } - } + }); } } } - // Entity contact tracking - if (!entityContactCallbacks.isEmpty()) { + // 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++) { @@ -271,28 +319,56 @@ public void fireTick() { ServerPlayer b = players.get(j); double dist = a.distanceToSqr(b); String pairKey = a.getStringUUID() + "|" + b.getStringUUID(); - if (dist < 2.25) { // 1.5 blocks squared - if (entityContactPairs.add(pairKey)) { + if (dist < 2.25) { + if (pairs.add(pairKey)) { Box3JSEntity ea = new Box3JSEntity(a, server, this); Box3JSEntity eb = new Box3JSEntity(b, server, this); - for (var cb : entityContactCallbacks) { - cb.onContact(ea, eb, tick); - } - } - } else if (entityContactPairs.remove(pairKey)) { - if (!entitySeparateCallbacks.isEmpty()) { - Box3JSEntity ea = new Box3JSEntity(a, server, this); - Box3JSEntity eb = new Box3JSEntity(b, server, this); - for (var cb : entitySeparateCallbacks) { - cb.onSeparate(ea, eb, tick); - } + 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(); @@ -300,96 +376,160 @@ private String getBlockIdString(BlockPos pos) { } public void fireVoxelDestroy(ServerPlayer player, BlockPos pos) { - if (voxelDestroyCallbacks.isEmpty()) return; - Box3JSEntity entity = new Box3JSEntity(player, server, this); - long tick = server.getTickCount(); - String voxel = getBlockIdString(pos); - for (var cb : voxelDestroyCallbacks) { - cb.onDestroy(entity, pos.getX(), pos.getY(), pos.getZ(), voxel, tick); + 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) { - if (interactCallbacks.isEmpty()) return; - Box3JSEntity entity = new Box3JSEntity(player, server, this); - Box3JSEntity targetEntity = new Box3JSEntity(target, server, this); - long tick = server.getTickCount(); - for (var cb : interactCallbacks) { - cb.onInteract(entity, targetEntity, tick); + 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 = new Box3JSEntity(player, server, this); - long tick = server.getTickCount(); - // Global chat callbacks - for (var cb : chatCallbacks) { - cb.onChat(entity, message, tick); + 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 handler - Function playerHandler = playerChatHandlers.get(player.getUUID()); - if (playerHandler != null) { - callFunction(playerHandler, entity, message, tick); + // 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) { - if (blockPlaceCallbacks.isEmpty()) return; - Box3JSEntity entity = new Box3JSEntity(player, server, this); - long tick = server.getTickCount(); - int voxelId = voxelsBinding.getId(state); - String voxel = state.isAir() ? "minecraft:air" : state.getBlock().builtInRegistryHolder().key().location().toString(); - for (var cb : blockPlaceCallbacks) { - cb.onPlace(entity, pos.getX(), pos.getY(), pos.getZ(), voxel, voxelId, tick); + 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) { - if (entityDeathCallbacks.isEmpty()) return; - Box3JSEntity entity = new Box3JSEntity(deadEntity, server, this); - Box3JSEntity killer = attacker != null ? new Box3JSEntity(attacker, server, this) : null; - long tick = server.getTickCount(); - for (var cb : entityDeathCallbacks) { - cb.onDeath(entity, killer, tick); + 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) { - if (respawnCallbacks.isEmpty()) return; - Box3JSEntity entity = new Box3JSEntity(player, server, this); - for (var cb : respawnCallbacks) { - cb.onRespawn(entity); + 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) { - if (blockActivateCallbacks.isEmpty()) return; - Box3JSEntity entity = new Box3JSEntity(player, server, this); - long tick = server.getTickCount(); - String voxel = state.isAir() ? "minecraft:air" : state.getBlock().builtInRegistryHolder().key().location().toString(); - for (var cb : blockActivateCallbacks) { - cb.onActivate(entity, pos.getX(), pos.getY(), pos.getZ(), voxel, tick); + 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) { - if (entityDamageCallbacks.isEmpty()) return; - Box3JSEntity entity = new Box3JSEntity(damagedEntity, server, this); - Box3JSEntity attackerEntity = attacker != null ? new Box3JSEntity(attacker, server, this) : null; - long tick = server.getTickCount(); - for (var cb : entityDamageCallbacks) { - cb.onDamage(entity, amount, source, attackerEntity, tick); + 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 = new Box3JSEntity(player, server, this); - for (var cb : joinCallbacks) cb.onJoin(entity); + 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 = new Box3JSEntity(player, server, this); - for (var cb : leaveCallbacks) cb.onLeave(entity); + 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 */ @@ -410,39 +550,16 @@ public Object wrap(Object obj) { public ScriptableObject getScope() { return scope; } public Map getCustomProps(UUID uuid) { - return entityCustomProps.computeIfAbsent(uuid, k -> new HashMap<>()); + return bus.entityCustomProps.computeIfAbsent(uuid, k -> new HashMap<>()); } public void clearCustomProps(UUID uuid) { - entityCustomProps.remove(uuid); + bus.entityCustomProps.remove(uuid); } /** Clear all callbacks and reset the JS scope (keeps server binding) */ public void reset() { - 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(); - timerIdCounter = 0; + bus.clearAll(); projectRequires.clear(); this.worldBinding = new Box3JSWorld(server, this); this.voxelsBinding = new Box3JSVoxels(server); @@ -565,7 +682,7 @@ public void clear() { } } - private static class TimerEntry { + static class TimerEntry { final int id; final Function handler; int remaining; 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..d3dca192 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java @@ -0,0 +1,37 @@ +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", + "build.mjs", + "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/resources/assets/box3js/template/types/globals.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/globals.d.ts index cb160aab..afbfedba 100644 --- 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 @@ -528,21 +528,32 @@ interface ReturnValue { /** * 全局存储入口 — 脚本中通过 `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. + * 打开或创建指定名称的数据存储空间 (项目隔离)。 + * 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; /** - * 行为与 getDataStorage 相同 (Box3 兼容别名)。 - * Same as getDataStorage — Box3 compatibility alias. + * 获取跨项目共享存储 — 所有项目通过同一 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; } From 92e50018fca1e7e072521234239ebadb378ee535 Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Wed, 6 May 2026 17:54:32 +0800 Subject: [PATCH 13/17] =?UTF-8?q?feat(api):=20=E6=B7=BB=E5=8A=A0=E6=B2=99?= =?UTF-8?q?=E7=AE=B1=E6=A8=A1=E5=BC=8F=E5=92=8C=E4=BA=8C=E6=AE=B5=E8=B7=B3?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加沙箱模式命令,用于追踪和回滚脚本更改 - 实现二段跳 API,包含 canDoubleJump 和 doubleJumpPower 属性 - 添加项目级的 bossbar、记分板和队伍管理 - 支持停止脚本时按项目清理资源 - 为命令中的项目名称添加 Tab 自动补全 - 更新文档以反映新功能和改进的命令用法 --- Box3JS-NeoForge-1.21.1/docs/api/README.md | 4 +- Box3JS-NeoForge-1.21.1/docs/api/commands.md | 64 ++- Box3JS-NeoForge-1.21.1/docs/api/player.md | 38 +- .../box3lab/box3js/script/Box3JSBossbar.java | 34 +- .../box3lab/box3js/script/Box3JSEntity.java | 23 +- .../box3lab/box3js/script/Box3JSPlayer.java | 46 +- .../box3js/script/Box3JSScoreboard.java | 36 +- .../com/box3lab/box3js/script/Box3JSTeam.java | 33 +- .../box3lab/box3js/script/Box3JSVoxels.java | 8 +- .../box3lab/box3js/script/Box3JSWorld.java | 47 +- .../box3js/script/Box3ScriptCommand.java | 107 +++- .../box3js/script/Box3ScriptEngine.java | 22 +- .../box3js/script/Box3ScriptSandbox.java | 457 ++++++++++++++++++ .../box3js/script/Box3ScriptTemplate.java | 1 - .../assets/box3js/template/build.mjs | 169 ++++++- .../assets/box3js/template/gitignore.template | 1 + .../assets/box3js/template/package.json | 11 +- .../assets/box3js/template/types/globals.d.ts | 22 + README.md | 55 +++ 19 files changed, 1071 insertions(+), 107 deletions(-) create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptSandbox.java diff --git a/Box3JS-NeoForge-1.21.1/docs/api/README.md b/Box3JS-NeoForge-1.21.1/docs/api/README.md index 5bbaa7b1..901c3dd4 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/README.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/README.md @@ -25,7 +25,7 @@ console.log("脚本已加载"); | 对象 | 类型 | 说明 | |---|---|---| | `world` | ✅ Box3 | 世界控制,见 [world.md](world.md) | -| `entity` | ✅ Box3 | 实体包装,见 [entity.md](entity.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) | @@ -82,7 +82,7 @@ var buildCourse = require("./course").buildCourse; var startRace = require("./game").startRace; ``` -> 注意:`require()` 使用 Rhino 内置的 CommonJS 模块系统,模块会被缓存供后续导入。仅在 `/box3script run ` 和自动加载时可用(需要项目上下文)。 +> 注意:`require()` 使用 Rhino 内置的 CommonJS 模块系统,模块会被缓存供后续导入。仅在脚本加载执行时可用(需要项目上下文)。 ## Tick 与性能 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/commands.md b/Box3JS-NeoForge-1.21.1/docs/api/commands.md index 8eff9a0f..74d5793a 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/commands.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/commands.md @@ -1,6 +1,6 @@ # /box3script 命令参考 -所有命令需要 **OP 权限等级 2**(默认管理员权限)。 +所有命令需要 **OP 权限等级 2**(默认管理员权限)。所有 `` 参数均支持 **Tab 自动补全**。 --- @@ -36,20 +36,20 @@ npm install npm run build # 输出 dist/app.js ``` -### `/box3script list` +### `/box3script` -列出所有已发现的脚本项目及其启用/禁用状态。 +直接输入不带参数,列出所有项目及启用/禁用/沙盒状态。 ``` -/box3script list +/box3script ``` 输出示例: ``` -项目列表: - [开] skyrun - [关] siege - [关] mygame +=== Projects === + [ON] [SANDBOX] colorzone + [ON] demo + [OFF] siege ``` ### `/box3script on ` @@ -86,12 +86,20 @@ npm run build # 输出 dist/app.js ### `/box3script reload` -停止所有脚本,重新加载所有已启用项目的 `app.js`。等价于 `stop` + 重新 `autoLoad`。加载错误会反馈到聊天栏。 +停止所有脚本,重新加载所有已启用项目的 `app.js`。加载错误会反馈到聊天栏。 ``` /box3script reload ``` +### `/box3script reload ` + +重新加载指定项目(先停止再启动)。未启用的项目会自动设为启用后启动。开发调试时比 `stop` + `on` 更快。 + +``` +/box3script reload colorzone +``` + ### `/box3script watch` 开启/关闭文件监控。开启后监控所有项目的 `dist/` 目录,`.js` 文件变化时自动热重载对应项目(2 秒防抖)。 @@ -102,9 +110,43 @@ npm run build # 输出 dist/app.js /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 @@ -112,7 +154,7 @@ npm run build # 输出 dist/app.js ### `/box3script stop ` -停止指定项目,仅清除该项目的回调、定时器和作用域,**不影响其他正在运行的项目**。 +停止指定项目,仅清除该项目的回调、定时器和作用域,**不影响其他正在运行的项目**。沙盒项目会保留追踪状态,不会回滚。 ``` /box3script stop siege diff --git a/Box3JS-NeoForge-1.21.1/docs/api/player.md b/Box3JS-NeoForge-1.21.1/docs/api/player.md index 5309b788..5d62bff6 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/player.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/player.md @@ -23,12 +23,13 @@ world.onPlayerJoin((entity) => { ### player.getOpLevel() -⬆ MC 扩展 | 返回玩家管理员权限等级 (0-4)。0=普通玩家, 1=可绕过出生点保护, 2=可使用大部分命令, 3=可管理玩家, 4=最高权限。 +⬆ MC 扩展 | 获取/设置玩家管理员权限等级 (0-4)。0=普通玩家, 1=可绕过出生点保护, 2=可使用大部分命令, 3=可管理玩家, 4=最高权限。 ```js if (player.getOpLevel() >= 2) { // 需要权限等级 2 的操作 } +player.opLevel = 3; // 设置为 3 级权限 ``` --- @@ -190,6 +191,38 @@ var target = player.cameraTarget; --- +## 二段跳 + +全部 ⬆ MC 扩展。 + +### player.canDoubleJump + +获取/设置是否允许二段跳。设为 `true` 后玩家在空中可再跳一次。 + +### player.doubleJumpPower + +二段跳的垂直力度,默认 `0.42`(与普通跳跃一致)。调大可以跳得更高。 + +### player.doubleJump() + +执行二段跳(需在 tick 回调中调用)。仅在 `canDoubleJump = true` 且玩家在空中、且本跳未使用过时才生效。落地自动重置。 + +```js +// 需要在 tick 中持续调用 +world.onTick(() => { + player.doubleJump(); +}); +``` + +典型用法 — 在 colorzone 中启用二段跳: + +```js +player.canDoubleJump = true; +player.doubleJumpPower = 0.6; // 比普通跳跃高 +``` + +--- + ## 传送与重生 ### player.teleport(pos) @@ -475,4 +508,5 @@ player.setPlayerListName(player.name); | `lookAt()` | ⬆ MC | | `runCommand()` | ⬆ MC | | `setPlayerListName()` | ⬆ MC | -| `getOpLevel()` | ⬆ MC | +| `getOpLevel()` / `opLevel` | ⬆ MC | +| `canDoubleJump` / `doubleJumpPower` / `doubleJump()` | ⬆ MC | 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 index 8a4e6e0f..deb95e9d 100644 --- 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 @@ -5,27 +5,25 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.BossEvent.BossBarColor; import net.minecraft.world.BossEvent.BossBarOverlay; -import net.minecraft.world.entity.player.Player; import net.minecraft.server.level.ServerBossEvent; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; +import java.util.*; class Box3JSBossbar { private final MinecraftServer server; - private final Map bossBars = new HashMap<>(); + private final Map> projectBossBars = new HashMap<>(); Box3JSBossbar(MinecraftServer server) { this.server = server; } - void showBossbar(String name, String text, double progress, String colorName) { - ServerBossEvent bar = bossBars.get(name); + 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); - bossBars.put(name, bar); + bars.put(name, bar); } else { bar.setName(Component.literal(text)); if (colorName != null) bar.setColor(resolveColor(colorName)); @@ -34,11 +32,27 @@ void showBossbar(String name, String text, double progress, String colorName) { for (ServerPlayer sp : server.getPlayerList().getPlayers()) bar.addPlayer(sp); } - void removeBossbar(String name) { - ServerBossEvent bar = bossBars.remove(name); + 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)) { 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 index 18d8756a..d26d1575 100644 --- 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 @@ -83,6 +83,7 @@ public GameVector3 getBounds() { public boolean getMeshInvisible() { return getProp("meshInvisible", false); } public void setMeshInvisible(boolean v) { + trackIfSandboxed(); setProp("meshInvisible", v); entity.setInvisible(v); } @@ -90,6 +91,7 @@ public void setMeshInvisible(boolean v) { // ---- Tags ---- public void addTag(String tag) { + trackIfSandboxed(); entity.addTag(tag); } @@ -98,13 +100,14 @@ public boolean hasTag(String tag) { } public void removeTag(String tag) { + trackIfSandboxed(); entity.removeTag(tag); } // ---- Glowing (MC extension) ---- public boolean isGlowing() { return entity.isCurrentlyGlowing(); } - public void setGlowing(boolean v) { entity.setGlowingTag(v); } + public void setGlowing(boolean v) { trackIfSandboxed(); entity.setGlowingTag(v); } // ---- Name tag (MC extension) ---- @@ -114,6 +117,7 @@ public String getNameTag() { } public void setNameTag(String name) { + trackIfSandboxed(); entity.setCustomName(net.minecraft.network.chat.Component.literal(name)); entity.setCustomNameVisible(true); } @@ -137,6 +141,7 @@ public double getHp() { return getProp("hp", 100.0); } public void setHp(double v) { + trackIfSandboxed(); setProp("hp", v); LivingEntity le = asLiving(); if (le != null) { @@ -151,6 +156,7 @@ public double getMaxHp() { return getProp("maxHp", 100.0); } public void setMaxHp(double v) { + trackIfSandboxed(); setProp("maxHp", v); LivingEntity le = asLiving(); if (le != null) { @@ -174,14 +180,19 @@ 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) { entity.setInvulnerable(v); } + public void setInvulnerable(boolean v) { trackIfSandboxed(); entity.setInvulnerable(v); } // ---- Fire (MC extension) ---- public void setFire(int ticks) { + trackIfSandboxed(); entity.setRemainingFireTicks(ticks); } @@ -208,6 +219,7 @@ public boolean navigateTo(GameVector3 pos, double 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); } @@ -215,6 +227,7 @@ public void setTarget(Box3JSEntity target) { /** Clear the mob's attack target, stopping pursuit. */ public void clearTarget() { + trackIfSandboxed(); if (entity instanceof Mob mob) { mob.setTarget(null); } @@ -231,6 +244,7 @@ public Box3JSEntity getTarget() { /** Enable or disable the mob's AI (pathfinding, goals, etc.) */ public void setAI(boolean enabled) { + trackIfSandboxed(); if (entity instanceof Mob mob) { mob.setNoAi(!enabled); } @@ -243,6 +257,7 @@ public void addEffect(String effectId, int duration, int amplifier) { } public void addEffect(String effectId, int duration, int amplifier, boolean hideParticles) { + trackIfSandboxed(); LivingEntity le = asLiving(); if (le == null) return; Holder effect = Box3ScriptUtils.lookupMobEffect(effectId); @@ -253,6 +268,7 @@ public void addEffect(String effectId, int duration, int amplifier, boolean hide // ---- 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; @@ -264,6 +280,7 @@ public void setEquipment(String slot, String itemId) { // ---- 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)) { @@ -279,6 +296,7 @@ public void setDropChance(String slot, double chance) { // ---- Persistence (MC extension) ---- public void setPersistent(boolean v) { + trackIfSandboxed(); if (entity instanceof Mob mob && v) mob.setPersistenceRequired(); } @@ -293,6 +311,7 @@ public double getAttribute(String attributeId) { } public void setAttribute(String attributeId, double value) { + trackIfSandboxed(); LivingEntity le = asLiving(); if (le == null) return; var holder = Box3ScriptUtils.lookupAttribute(attributeId); 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 index 57f726b0..b7dbd797 100644 --- 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 @@ -43,10 +43,19 @@ public Box3JSPlayer(ServerPlayer player, MinecraftServer server, Box3ScriptEngin 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) { player.setInvisible(v); } + public void setInvisible(boolean v) { trackIfSandboxed(); player.setInvisible(v); } public double getScale() { return player.getScale(); } @@ -54,16 +63,19 @@ public Box3JSPlayer(ServerPlayer player, MinecraftServer server, Box3ScriptEngin 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); } @@ -89,11 +101,13 @@ public String getWalkState() { 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); } @@ -102,6 +116,7 @@ public boolean getCollision() { 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 @@ -114,6 +129,7 @@ public void setCollision(boolean enabled) { public double getFlySpeed() { return player.getAbilities().getFlyingSpeed(); } public void setFlySpeed(double v) { + trackIfSandboxed(); updateAbility(a -> a.setFlyingSpeed((float) v)); } @@ -121,6 +137,7 @@ public void setFlySpeed(double v) { 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()); @@ -148,6 +165,7 @@ public void setDimension(String dimId) { 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; } } @@ -379,6 +397,7 @@ public void addEffect(String effectId, int duration, int amplifier) { } 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)); @@ -386,6 +405,7 @@ public void addEffect(String effectId, int duration, int amplifier, boolean hide } public void clearEffects() { + trackIfSandboxed(); player.removeAllEffects(); } @@ -398,8 +418,32 @@ public void playSound(String path, double volume, double pitch) { } } + // ---- Double Jump ---- + + public boolean getCanDoubleJump() { return getProp("canDoubleJump", false); } + public void setCanDoubleJump(boolean v) { trackIfSandboxed(); setProp("canDoubleJump", v); } + + public double getDoubleJumpPower() { return getProp("doubleJumpPower", 0.42); } + public void setDoubleJumpPower(double v) { setProp("doubleJumpPower", v); } + + public void doubleJump() { + if (player.onGround()) { + setProp("hasDoubleJumped", false); + } + if (getCanDoubleJump() && !getProp("hasDoubleJumped", false) && !player.onGround()) { + double power = getDoubleJumpPower(); + player.setDeltaMovement(player.getDeltaMovement().x, power, player.getDeltaMovement().z); + player.hurtMarked = true; + setProp("hasDoubleJumped", true); + } + } + // ---- Custom properties ---- + private void trackIfSandboxed() { + engine.getSandbox().trackPlayer(engine.getCurrentProject(), player); + } + private Map props() { return engine.getCustomProps(player.getUUID()); } 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 index 41fbf888..127e68cc 100644 --- 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 @@ -12,32 +12,36 @@ import net.minecraft.world.scores.Scoreboard; import net.minecraft.world.scores.criteria.ObjectiveCriteria; -import java.util.ArrayList; -import java.util.List; +import java.util.*; class Box3JSScoreboard { private final MinecraftServer server; + private final Map> projectObjectives = new HashMap<>(); Box3JSScoreboard(MinecraftServer server) { this.server = server; } - void addScoreboard(String name) { addScoreboard(name, "dummy"); } + void addScoreboard(String project, String name) { addScoreboard(project, name, "dummy"); } - void addScoreboard(String name, String criteria) { + 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); + if (obj != null) { + sb.removeObjective(obj); + for (Set set : projectObjectives.values()) set.remove(name); + } } void setScore(Object entityOrName, String objectiveName, int value) { @@ -85,6 +89,28 @@ List listScores(String objectiveName) { 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; 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 index f02e7aea..07349b3d 100644 --- 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 @@ -6,15 +6,18 @@ 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 name, String colorName) { + void createTeam(String project, String name, String colorName) { Scoreboard sb = server.getScoreboard(); if (sb.getPlayerTeam(name) != null) return; PlayerTeam team = sb.addPlayerTeam(name); @@ -23,12 +26,16 @@ void createTeam(String name, String colorName) { 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); + if (team != null) { + sb.removePlayerTeam(team); + for (Set set : projectTeams.values()) set.remove(name); + } } void joinTeam(Object entityOrName, String teamName) { @@ -52,4 +59,26 @@ String getTeamOf(Object entityOrName) { 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 index e305c1f4..36236f97 100644 --- 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 @@ -33,6 +33,7 @@ public class Box3JSVoxels { ); 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<>(); @@ -42,8 +43,9 @@ public class Box3JSVoxels { public final GameVector3 shape; public final String[] VoxelTypes; - public Box3JSVoxels(MinecraftServer server) { + public Box3JSVoxels(MinecraftServer server, Box3ScriptSandbox sandbox) { this.server = server; + this.sandbox = sandbox; nameToId.put("air", 0); idToName.put(0, "air"); @@ -114,6 +116,8 @@ 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 @@ -186,6 +190,8 @@ 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; 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 index aeeadd10..7f109143 100644 --- 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 @@ -49,6 +49,24 @@ public Box3JSWorld(MinecraftServer server, Box3ScriptEngine 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(); } @@ -56,14 +74,16 @@ public Box3JSWorld(MinecraftServer server, Box3ScriptEngine engine) { public int currentTick() { return server.getTickCount(); } public double getRainDensity() { return server.overworld().getRainLevel(1.0f); } - public void setRainDensity(double v) { server.overworld().getLevelData().setRaining(v > 0); } + 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); @@ -72,12 +92,13 @@ public void clearWeather() { // ---- Time ---- public long getTime() { return server.overworld().getDayTime(); } - public void setTime(long tick) { server.overworld().setDayTime(tick); } + 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); } @@ -85,6 +106,7 @@ public void setTimeScale(double v) { 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); } @@ -106,6 +128,7 @@ public Object getGameRule(String name) { } 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; @@ -137,6 +160,7 @@ public Box3JSEntity spawnEntity(String type, GameVector3 pos) { 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); } @@ -235,8 +259,8 @@ public void runCommand(String cmd) { // ---- Scoreboard ---- - public void addScoreboard(String name) { scoreboard.addScoreboard(name); } - public void addScoreboard(String name, String criteria) { scoreboard.addScoreboard(name, criteria); } + 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); } @@ -246,12 +270,12 @@ public void runCommand(String cmd) { // ---- Boss Bar ---- - public void showBossbar(String name, String text, double progress, String colorName) { bossbar.showBossbar(name, text, progress, colorName); } - public void removeBossbar(String name) { bossbar.removeBossbar(name); } + 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(name, colorName); } + 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); } @@ -260,14 +284,15 @@ public void runCommand(String cmd) { // ---- World Border ---- public double getBorderSize() { return server.overworld().getWorldBorder().getSize(); } - public void setBorderCenter(double x, double z) { server.overworld().getWorldBorder().setCenter(x, z); } - public void setBorderSize(double size) { server.overworld().getWorldBorder().setSize(size); } + 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) { server.overworld().getWorldBorder().setDamagePerBlock(damage); } - public void setBorderWarning(int blocks) { server.overworld().getWorldBorder().setWarningBlocks(blocks); } + public void setBorderDamage(double damage) { trackIfSandboxed(); server.overworld().getWorldBorder().setDamagePerBlock(damage); } + public void setBorderWarning(int blocks) { trackIfSandboxed(); server.overworld().getWorldBorder().setWarningBlocks(blocks); } // ---- Lightning ---- 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 index 4668c1ea..e726e6a6 100644 --- 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 @@ -2,6 +2,9 @@ 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; @@ -10,6 +13,7 @@ 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; @@ -23,16 +27,38 @@ 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(listCommand()) .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. */ @@ -90,6 +116,7 @@ private static LiteralArgumentBuilder stopCommand() { Box3ScriptEngine.get().reset() )) .then(argument("project", StringArgumentType.word()) + .suggests(Box3ScriptCommand::suggestProjects) .executes(ctx -> { String project = StringArgumentType.getString(ctx, "project"); Box3ScriptConfig.get().setEnabled(project, false); @@ -99,35 +126,12 @@ private static LiteralArgumentBuilder stopCommand() { })); } - // --- list --- - - private static LiteralArgumentBuilder listCommand() { - return literal("list") - .executes(ctx -> { - var config = Box3ScriptConfig.get(); - config.discover(ctx.getSource().getServer()); - var projects = config.listProjects(); - if (projects.isEmpty()) { - ctx.getSource().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]"; - sb.append(" ").append(status).append(" §f").append(name).append("\n"); - }); - ctx.getSource().sendSuccess( - () -> Component.literal(sb.toString().trim()), false); - } - return 1; - }); - } - // --- 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); @@ -155,6 +159,7 @@ private static LiteralArgumentBuilder onCommand() { 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); @@ -183,7 +188,25 @@ private static LiteralArgumentBuilder reloadCommand() { } 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 --- @@ -224,8 +247,40 @@ private static LiteralArgumentBuilder watchCommand() { })); } + // --- 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") 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 index be80a474..e2121d51 100644 --- 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 @@ -27,6 +27,7 @@ public class Box3ScriptEngine { private Box3JSWorld worldBinding; private Box3JSVoxels voxelsBinding; private Box3JSStorage storageBinding; + private Box3ScriptSandbox sandbox; private MinecraftServer server; private boolean initialized; @@ -42,8 +43,9 @@ public static Box3ScriptEngine get() { 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); + this.voxelsBinding = new Box3JSVoxels(server, sandbox); this.storageBinding = new Box3JSStorage(server.getServerDirectory().resolve("config"), this); setupScope(); initialized = true; @@ -199,12 +201,19 @@ public void setCurrentProject(String name) { } public String getCurrentProject() { return currentProject; } + Box3ScriptSandbox getSandbox() { return sandbox; } + // ---- Project lifecycle ---- - /** Remove one project's callbacks without affecting others. */ + /** 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); } @@ -557,12 +566,17 @@ public void clearCustomProps(UUID uuid) { bus.entityCustomProps.remove(uuid); } - /** Clear all callbacks and reset the JS scope (keeps server binding) */ + /** 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); + this.voxelsBinding = new Box3JSVoxels(server, sandbox); this.storageBinding = new Box3JSStorage(server.getServerDirectory().resolve("config"), this); setupScope(); } 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 index d3dca192..264b3fea 100644 --- 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 @@ -12,7 +12,6 @@ public class Box3ScriptTemplate { "gitignore.template", "package.json", "tsconfig.json", - "build.mjs", "src/app.ts", "types/globals.d.ts", }; 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 index e4874fef..6bde02f6 100644 --- 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 @@ -1,34 +1,155 @@ import * as esbuild from "esbuild"; -import { resolve, dirname } from "path"; +import { resolve, dirname, relative } from "path"; import { fileURLToPath } from "url"; -import { writeFileSync, mkdirSync } from "fs"; +import { + writeFileSync, + mkdirSync, + readdirSync, + statSync, + rmSync, + existsSync, + readFileSync, + watch as fsWatch, +} from "fs"; import babel from "@babel/core"; 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"); +const isWatchMode = process.argv.includes("--watch"); -const result = await esbuild.build({ - entryPoints: [resolve(srcDir, "app.ts")], - outfile: resolve(distDir, "app.js"), - bundle: true, - format: "cjs", - target: "rhino1.9.1", - platform: "neutral", - minify: false, - write: false, - supported: { - class: true, - }, -}); - -for (const out of result.outputFiles) { - let code = out.text; - const transformed = babel.transformSync(code, { - presets: [["@babel/preset-env", { targets: { ie: "11" }, modules: false }]], - configFile: false, +const HELPER_REGEX_LITERAL = + "/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t)"; +const HELPER_TYPED_ARRAY_CHECK = + '(t === "Int8Array" || t === "Uint8Array" || t === "Uint8ClampedArray" || t === "Int16Array" || t === "Uint16Array" || t === "Int32Array" || t === "Uint32Array")'; + +function cleanupTempDir() { + if (existsSync(tempDir)) rmSync(tempDir, { recursive: true, force: true }); +} + +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; +} + +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: "commonjs", + 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"); + } +} + +async function bundleAndSanitize() { + await esbuild.build({ + entryPoints: [resolve(tempDir, "app.js")], + outfile: distFile, + bundle: true, + format: "cjs", + platform: "neutral", + target: ["rhino1.9.1"], + minify: false, + write: true, + logLevel: "info", }); - code = transformed.code; - mkdirSync(dirname(out.path), { recursive: true }); - writeFileSync(out.path, code, "utf-8"); + + const code = readFileSync(distFile, "utf-8"); + const sanitized = code + .split(HELPER_REGEX_LITERAL) + .join(HELPER_TYPED_ARRAY_CHECK); + writeFileSync(distFile, sanitized, "utf-8"); +} + +async function buildOnce() { + await transpileTsToTemp(); + await bundleAndSanitize(); +} + +if (!isWatchMode) { + try { + await buildOnce(); + } finally { + cleanupTempDir(); + } +} else { + let timer = null; + let building = false; + let pending = false; + let closing = false; + + 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 index deed335b..c2b6ab3d 100644 --- 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 @@ -1,3 +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 index d90db519..be5865fd 100644 --- 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 @@ -4,14 +4,15 @@ "private": true, "type": "module", "scripts": { + "dev": "node build.mjs --watch", "build": "node build.mjs", - "check": "tsc --noEmit", - "dev": "node build.mjs --watch" + "check": "tsc --noEmit" }, - "devDependencies": { + "dependencies": { "@babel/core": "^7.29.0", - "@babel/preset-env": "^7.29.2", + "@babel/preset-env": "^7.29.5", + "@babel/preset-typescript": "^7.28.5", "esbuild": "^0.28.0", - "typescript": "^5.7.0" + "typescript": "^6.0.3" } } 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 index afbfedba..f665749a 100644 --- 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 @@ -684,6 +684,7 @@ interface GameEntity { * Custom name tag text (empty string = none). */ nameTag: string; + setNameTag(name: string): void; // ── 无敌 & 持久化 / Invulnerability & Persistence ── @@ -883,6 +884,27 @@ interface GamePlayer { */ jumpPower: number; + /** + * 是否允许二段跳。 + * Whether double‑jump is enabled for this player. + */ + canDoubleJump: boolean; + + /** + * 二段跳力度 (默认 0.42, 等同于普通跳跃)。 + * Double‑jump power (default 0.42, same as a normal jump). + */ + doubleJumpPower: number; + + /** + * 执行二段跳 — 仅在玩家离地且 canDoubleJump 为 true 时生效。 + * 每次落地后自动重置, 同一滞空时间只能二段跳一次。 + * + * Performs a double jump — only works when the player is off the ground + * and canDoubleJump is true. Resets automatically on landing. + */ + doubleJump(): void; + /** * 当前移动状态。 * Current movement state. diff --git a/README.md b/README.md index d19554f0..e8d2eee8 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` 权限等级。 From f76e7ee5408753da3485359e26419d3a4b50b914 Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Wed, 6 May 2026 18:12:52 +0800 Subject: [PATCH 14/17] =?UTF-8?q?feat(types):=20=E4=B8=BA=20GamePlayer=20?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=B7=BB=E5=8A=A0=E7=8E=A9=E5=AE=B6=E7=94=9F?= =?UTF-8?q?=E5=91=BD=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 hp 属性表示当前生命值 - 添加 maxHp 属性表示最大生命值 - 包含中英文文档注释 --- .../main/resources/assets/box3js/template/types/globals.d.ts | 5 +++++ 1 file changed, 5 insertions(+) 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 index f665749a..f99f732d 100644 --- 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 @@ -998,6 +998,11 @@ interface GamePlayer { /** 饱和度 (0‑20)。Saturation level (0–20). */ saturation: number; + /** 当前生命值。Current health. */ + hp: number; + /** 最大生命值。Maximum health. */ + maxHp: number; + // ── 经验 / Experience ── /** 经验等级 (与 /xp 命令相同)。Experience level (same as /xp command). */ From b35e3367e5e82704230556ca1b624cde6180f16c Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Wed, 6 May 2026 18:36:18 +0800 Subject: [PATCH 15/17] =?UTF-8?q?feat(player):=20=E4=B8=BA=20Box3JSPlayer?= =?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=E7=94=9F=E5=91=BD=E5=80=BC=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 getHp()、setHp()、getMaxHp() 和 setMaxHp() 方法 - 实现适当的生命值验证和钳位处理 - 为生命值修改操作添加沙箱追踪 perf(build): 通过正则替换优化构建脚本 - 将静态字符串替换改为动态正则匹配 - 添加 typedArrayCheck 函数以提高可维护性 - 在打包过程中启用压缩 chore(tsconfig): 更新 TypeScript 配置 - 将 target 和 module 升级为 ESNext - 添加 ESNext 库支持 --- .../box3lab/box3js/script/Box3JSPlayer.java | 19 +++++++++++++++++++ .../assets/box3js/template/build.mjs | 15 +++++++-------- .../assets/box3js/template/tsconfig.json | 5 +++-- 3 files changed, 29 insertions(+), 10 deletions(-) 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 index b7dbd797..b3b2bc3c 100644 --- 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 @@ -323,6 +323,25 @@ public void runCommand(String cmd) { 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; } 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 index 6bde02f6..1b9ed241 100644 --- 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 @@ -20,10 +20,11 @@ const distDir = resolve(__dirname, "dist"); const distFile = resolve(distDir, "app.js"); const isWatchMode = process.argv.includes("--watch"); -const HELPER_REGEX_LITERAL = - "/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t)"; -const HELPER_TYPED_ARRAY_CHECK = - '(t === "Int8Array" || t === "Uint8Array" || t === "Uint8ClampedArray" || t === "Int16Array" || t === "Uint16Array" || t === "Int32Array" || t === "Uint32Array")'; +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")`; +} function cleanupTempDir() { if (existsSync(tempDir)) rmSync(tempDir, { recursive: true, force: true }); @@ -80,15 +81,13 @@ async function bundleAndSanitize() { format: "cjs", platform: "neutral", target: ["rhino1.9.1"], - minify: false, + minify: true, write: true, logLevel: "info", }); const code = readFileSync(distFile, "utf-8"); - const sanitized = code - .split(HELPER_REGEX_LITERAL) - .join(HELPER_TYPED_ARRAY_CHECK); + const sanitized = code.replace(BAD_REGEX, typedArrayCheck); writeFileSync(distFile, sanitized, "utf-8"); } 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 index bf3ed845..c3666879 100644 --- 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 @@ -1,8 +1,9 @@ { "compilerOptions": { - "target": "ES2015", - "module": "ES2015", + "target": "ESNext", + "module": "ESNext", "moduleResolution": "bundler", + "lib": ["ESNext"], "strict": true, "noEmit": true, "isolatedModules": true, From 5f0f09d1254c7d5a4a6367fe4266f62b1dcd194c Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Wed, 6 May 2026 19:26:12 +0800 Subject: [PATCH 16/17] =?UTF-8?q?docs(readme):=20=E6=9B=B4=E6=96=B0=20READ?= =?UTF-8?q?ME=EF=BC=8C=E5=A2=9E=E5=BC=BA=E9=A1=B9=E7=9B=AE=E6=8F=8F?= =?UTF-8?q?=E8=BF=B0=E5=B9=B6=E7=AE=80=E5=8C=96=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: 项目名称更新为包含中文副标题, 移除了安装章节,整合了 API 参考, 并将重载命令从 `/box3script reload` 更改为 `/box3script watch` --- Box3JS-NeoForge-1.21.1/README.md | 96 +-- Box3JS-NeoForge-1.21.1/README_en.md | 63 ++ Box3JS-NeoForge-1.21.1/docs/api/README.md | 114 ++- Box3JS-NeoForge-1.21.1/docs/api/README_en.md | 82 +++ Box3JS-NeoForge-1.21.1/docs/api/commands.md | 32 +- .../docs/api/commands_en.md | 189 +++++ Box3JS-NeoForge-1.21.1/docs/api/entity.md | 97 +-- Box3JS-NeoForge-1.21.1/docs/api/entity_en.md | 352 ++++++++++ Box3JS-NeoForge-1.21.1/docs/api/math.md | 110 ++- Box3JS-NeoForge-1.21.1/docs/api/math_en.md | 182 +++++ Box3JS-NeoForge-1.21.1/docs/api/player.md | 163 ++--- Box3JS-NeoForge-1.21.1/docs/api/player_en.md | 505 +++++++++++++ Box3JS-NeoForge-1.21.1/docs/api/storage.md | 75 +- Box3JS-NeoForge-1.21.1/docs/api/storage_en.md | 171 +++++ Box3JS-NeoForge-1.21.1/docs/api/voxels.md | 93 +-- Box3JS-NeoForge-1.21.1/docs/api/voxels_en.md | 181 +++++ Box3JS-NeoForge-1.21.1/docs/api/world.md | 285 +++----- Box3JS-NeoForge-1.21.1/docs/api/world_en.md | 662 ++++++++++++++++++ .../assets/box3js/template/build.mjs | 16 +- 19 files changed, 2761 insertions(+), 707 deletions(-) create mode 100644 Box3JS-NeoForge-1.21.1/README_en.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/api/README_en.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/api/commands_en.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/api/entity_en.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/api/math_en.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/api/player_en.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/api/storage_en.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/api/voxels_en.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/api/world_en.md diff --git a/Box3JS-NeoForge-1.21.1/README.md b/Box3JS-NeoForge-1.21.1/README.md index 933cfac3..22afc7a4 100644 --- a/Box3JS-NeoForge-1.21.1/README.md +++ b/Box3JS-NeoForge-1.21.1/README.md @@ -1,33 +1,19 @@ -# Box3JS +# Box3JS(神岛代码)-- Minecraft Mod > **测试版(Beta)** — 本项目处于早期测试阶段,API 可能变动,可能存在未发现的缺陷。欢迎反馈问题。 -**Box3JS** 是一个 Minecraft NeoForge 1.21.1 服务端模组,将 JavaScript 运行时(Mozilla Rhino 1.9.1)嵌入服务端。无需编写 Java 代码,用 JS/TS 即可编写服务端脚本——小游戏、机制扩展、自动化管理。 +[简体中文](README.md) | [English](README_en.md) ---- +`Box3JS` 是一个 Minecraft 服务端模组,延续了神奇代码岛的代码风格。你无需编写 Java,只需使用 TypeScript 即可开发脚本。 ## 特性 -- **JS 运行时** — Rhino 1.9.1 引擎,通过 Babel 向下兼容 ES5 -- **TypeScript 支持** — 项目模板内置 TS 类型声明,esbuild 打包,完整类型检查 +- **TypeScript 支持** — 项目模板内置 TS 类型声明,完整类型检查 - **Box3 API 兼容** — 实现了 Box3 平台核心 API(World / Entity / Player / Voxels / Storage) - **MC 扩展** — 90+ Minecraft 独有功能:记分板、Bossbar、队伍、世界边界、粒子、烟花、药水等 -- **CommonJS 模块** — `require()` 多文件组织,支持大型脚本项目 -- **热重载** — `/box3script reload` 重新加载,无需重启 +- **热重载** — `/box3script watch` 重新加载,无需重启 - **项目管理** — 多项目隔离,独立启用/禁用,重启自动执行 ---- - -## 安装 - -1. 将 JAR 放入服务端 `mods/` 目录 -2. 启动服务端 -3. 脚本目录自动创建在 `config/box3/script/` - -**需求:** NeoForge 1.21.1,Java 21 - ---- - ## 快速开始 在游戏中(需要 OP 权限,等级 ≥ 2): @@ -47,7 +33,7 @@ config/box3/script/mygame/ ├── types/ │ └── globals.d.ts ← Box3JS 完整类型声明 └── src/ - └── app.ts ← 入口 + └── app.ts ← 入口(含 Hello World 示例) ``` 然后构建: @@ -62,81 +48,15 @@ npm run build # 输出 dist/app.js ``` /box3script on mygame -/box3script reload ``` ---- - ## 可用 API -| 对象 | 说明 | 文档 | -|---|---|---| -| `world` | 世界状态、事件、记分板、Bossbar、队伍、粒子、烟花 | [world.md](docs/api/world.md) | -| `entity` | 实体属性、AI、装备、药水、标签 | [entity.md](docs/api/entity.md) | -| `player` | 玩家背包、消息、飞行、游戏模式、传送 | [player.md](docs/api/player.md) | -| `voxels` | 方块读写、区域填充 | [voxels.md](docs/api/voxels.md) | -| `storage` | JSON 数据持久化 | [storage.md](docs/api/storage.md) | -| 数学类型 | Vector3、Bounds3、Color、Quaternion | [math.md](docs/api/math.md) | - -[API 总览 →](docs/api/README.md) - ---- +[API 总览 →](docs/api/README.md) ([English](docs/api/README_en.md)) ## 命令 -| 命令 | 说明 | -|---|---| -| `/box3script create ` | 创建 TS 脚手架项目 | -| `/box3script run ` | 运行一次项目 | -| `/box3script list` | 列出所有项目及启用状态 | -| `/box3script on ` | 启用项目 | -| `/box3script off ` | 禁用项目 | -| `/box3script reload` | 重载所有已启用脚本 | -| `/box3script stop` | 停止所有脚本 | -| `/box3script file ` | 加载 JS 文件 | - -[命令详细参考 →](docs/api/commands.md) - ---- - -## 事件 - -```js -world.onTick(function () { ... }); -world.onPlayerJoin(function (entity) { ... }); -world.onPlayerLeave(function (entity) { ... }); -world.onChat(function (entity, message, tick) { ... }); -world.onEntityDeath(function (entity, killer, tick) { ... }); -world.onEntityDamage(function (entity, amount, source, attacker, tick) { ... }); -world.onPlayerRespawn(function (entity) { ... }); -world.onVoxelDestroy(function (entity, x, y, z, voxel, tick) { ... }); -world.onBlockPlace(function (entity, x, y, z, voxel, voxelId, tick) { ... }); -world.onBlockActivate(function (entity, x, y, z, voxel, tick) { ... }); -// 共 17 种事件,完整列表见 docs/api/world.md -``` - ---- - -## 已知限制(测试版) - -- 仅支持 NeoForge 1.21.1(Fabric / 其他 MC 版本暂未适配) -- Rhino 1.9.1 仅支持到 ES5 语法(class / 箭头函数 / 模板字符串由 Babel 转译) -- `player.dialog()` 为简化实现,仅发送系统消息 -- 部分 Box3 API(如 UI 相关)在服务端环境下不适用 -- 暂无自动化测试覆盖 - ---- - -## 构建 - -```bash -cd Box3JS-NeoForge-1.21.1 -./gradlew build -``` - -输出:`build/libs/box3js-.jar` - ---- +[命令详细参考 →](docs/api/commands.md) ([English](docs/api/commands_en.md)) ## 许可证 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..e6a3eac9 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/README_en.md @@ -0,0 +1,63 @@ +# Box3JS (Shendao Code) -- 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 that follows the coding style of Box3. You do not 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 reload 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/docs/api/README.md b/Box3JS-NeoForge-1.21.1/docs/api/README.md index 901c3dd4..84b1bc21 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/README.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/README.md @@ -1,20 +1,20 @@ # Box3JS API 参考 -Box3JS 是一个 Minecraft NeoForge 1.21.1 模组,允许用 JavaScript (Rhino 引擎) 编写服务端脚本。所有脚本运行在 `config/box3/script/<项目名>/app.js`。 +Box3JS 是一个 Minecraft 模组,允许用 JavaScript 编写服务端脚本。所有脚本运行在 `config/box3/script/<项目名>` 下。 ## 快速开始 ```js // app.js — 最简示例 world.onTick(() => { - // 每 tick 执行 (20 tick = 1 秒) + // 每 tick 执行 (20 tick = 1 秒) }); world.onChat((entity, message, tick) => { - var p = entity.player; - if (message === "!hello") { - p.directMessage("Hello, " + p.name + "!"); - } + var p = entity.player; + if (message === "!hello") { + p.directMessage("Hello, " + p.name + "!"); + } }); console.log("脚本已加载"); @@ -22,71 +22,61 @@ 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) | +| 对象 | 类型 | 说明 | +| ---------------- | ------- | -------------------------------------------------------------------------------- | +| `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 | +| 标注 | 含义 | +| --------------- | ------------------------------------------- | +| ✅ **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` 命令参考 | +| 文档 | 内容 | +| -------------------------- | ------------------------------------------------------- | +| [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` 命令参考 | -## 多文件模块 +## 文件模块 -使用 CommonJS 的 `require()` / `module.exports` 来组织和导入多文件项目。每个文件是一个独立模块,通过 `require("./模块名")` 导入(自动追加 `.js` 后缀): +**TypeScript 构建管线:** -``` -config/box3/script/skyrun/ -├── app.js ← 入口,require() 其他模块 -├── state.js ← 共享游戏状态 -├── course.js ← 赛道数据与建筑 -├── game.js ← 游戏流程控制 -├── checkpoints.js ← 检查点检测 -└── leaderboard.js ← 排行榜 -``` +`/box3script create` 创建的项目自带完整的 TS 构建环境。写入 `src/*.ts`,构建输出到 `dist/app.js`: -```js -// state.js — 导出共享状态 -var G = { phase: "lobby", checkpoints: [], ... }; -module.exports = { G: G, SB: "skyrun_scores" }; - -// app.js — 导入模块 -var G = require("./state").G; -var buildCourse = require("./course").buildCourse; -var startRace = require("./game").startRace; +``` +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 ← 编译产物(模组实际加载此文件) ``` -> 注意:`require()` 使用 Rhino 内置的 CommonJS 模块系统,模块会被缓存供后续导入。仅在脚本加载执行时可用(需要项目上下文)。 - -## Tick 与性能 - -- 服务端每秒 20 tick,`world.onTick()` 每 tick 触发 -- `world.setInterval(handler, ticks)` 可以降低频率,例如 `setInterval(fn, 20)` = 每秒 1 次 -- 避免在 tick 中执行大量方块操作或实体遍历 -- 使用 `world.setTimeout(handler, ticks)` 做延时操作 +`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 index 74d5793a..370c4555 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/commands.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/commands.md @@ -2,8 +2,6 @@ 所有命令需要 **OP 权限等级 2**(默认管理员权限)。所有 `` 参数均支持 **Tab 自动补全**。 ---- - ## 命令列表 ### `/box3script create ` @@ -15,6 +13,7 @@ ``` 生成的文件结构: + ``` config/box3/script/ └── mygame/ @@ -45,6 +44,7 @@ npm run build # 输出 dist/app.js ``` 输出示例: + ``` === Projects === [ON] [SANDBOX] colorzone @@ -57,7 +57,7 @@ npm run build # 输出 dist/app.js 启用指定项目并**立即加载执行**。加载错误会直接反馈到聊天栏。 ``` -/box3script on skyrun +/box3script on mygame ``` ### `/box3script on all` @@ -94,15 +94,15 @@ npm run build # 输出 dist/app.js ### `/box3script reload ` -重新加载指定项目(先停止再启动)。未启用的项目会自动设为启用后启动。开发调试时比 `stop` + `on` 更快。 +重新加载指定项目(先停止再启动)。未启用的项目会自动设为启用后启动。 ``` -/box3script reload colorzone +/box3script reload mygame ``` ### `/box3script watch` -开启/关闭文件监控。开启后监控所有项目的 `dist/` 目录,`.js` 文件变化时自动热重载对应项目(2 秒防抖)。 +开启/关闭文件监控。开启后监控所有项目的 `dist/` 目录,`.js` 文件变化时自动热重载对应项目。 ``` /box3script watch # 切换 开/关 @@ -114,20 +114,18 @@ npm run build # 输出 dist/app.js 切换沙盒模式。开启后自动追踪该项目所有的方块修改、实体/玩家/世界状态变更。**沙盒持久化**——`/box3script stop` 和 `/box3script reload` 不会清除沙盒状态,仅手动再次执行此命令才会关闭沙盒并回滚全部修改。关闭时在聊天栏显示恢复摘要。 -适合反复测试脚本,不用担心残留数据污染世界。 - ``` /box3script sandbox mygame # 切换 开/关 ``` **追踪内容:** -| 类别 | 追踪项 | -|---|---| -| 方块 | `setVoxel`/`setVoxelId`/`fillVoxel` 修改(上限 500 万块) | -| 实体 | HP、AI、隐身、发光、无敌、着火、药水效果、标签、名称、装备、掉落率、属性 | +| 类别 | 追踪项 | +| ---- | -------------------------------------------------------------------------------------- | +| 方块 | `setVoxel`/`setVoxelId`/`fillVoxel` 修改(上限 500 万块) | +| 实体 | HP、AI、隐身、发光、无敌、着火、药水效果、标签、名称、装备、掉落率、属性 | | 玩家 | 游戏模式、飞行能力、速度、跳跃力、经验、饱食度、物品栏、护甲、药水、位置、维度、重生点 | -| 世界 | 天气、时间、难度、游戏规则、世界边界 | +| 世界 | 天气、时间、难度、游戏规则、世界边界 | 典型工作流: @@ -160,29 +158,25 @@ npm run build # 输出 dist/app.js /box3script stop siege ``` ---- - ## 配置文件 启用/禁用状态保存在 `config/box3/scripts.json`: ```json { - "skyrun": true, + "mygame": true, "siege": false, "mygame": true } ``` ---- - ## 脚本目录结构 ``` config/box3/ ├── scripts.json ← 项目开关配置 ├── script/ ← 脚本目录 - │ ├── skyrun/ + │ ├── mygame/ │ │ ├── package.json │ │ ├── src/app.ts │ │ └── dist/app.js ← 编译产物 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 index db4da692..d8c1eef9 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/entity.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/entity.md @@ -4,8 +4,6 @@ 通过 `entity.player` 可获取该实体对应的 `player` 对象(仅当是玩家时有效)。 ---- - ## 基本属性 ### entity.id @@ -23,13 +21,11 @@ ```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()); + var e = all[i]; + console.log(e.id + " -> " + e.entityType + " -> isPlayer: " + e.isPlayer()); } ``` ---- - ## 位置与移动 ### entity.position @@ -62,7 +58,7 @@ entity.velocity.set(0, 1, 0); // 向上的速度 ```js if (entity.onGround) { - // 在地面 + // 在地面 } ``` @@ -74,8 +70,6 @@ if (entity.onGround) { var eye = entity.eyePosition; ``` ---- - ## 生命值 ### entity.hp @@ -101,8 +95,8 @@ zombie.hp = 100; ✅ Box3 API | 治疗实体 `amount` 点生命值(不超过 maxHp)。 ```js -zombie.hurt(10); // 造成 10 点伤害 -zombie.heal(5); // 治疗 5 点 +zombie.hurt(10); // 造成 10 点伤害 +zombie.heal(5); // 治疗 5 点 ``` ### entity.invulnerable @@ -110,12 +104,10 @@ zombie.heal(5); // 治疗 5 点 ⬆ MC 扩展 | 获取/设置实体是否无敌。 ```js -entity.invulnerable = true; // 不受伤害 +entity.invulnerable = true; // 不受伤害 console.log(entity.invulnerable); ``` ---- - ## 外观 ### entity.meshInvisible @@ -123,7 +115,7 @@ console.log(entity.invulnerable); ✅ Box3 API | 控制实体是否不可见。 ```js -entity.meshInvisible = true; // 隐身 +entity.meshInvisible = true; // 隐身 ``` ### entity.glowing @@ -131,7 +123,7 @@ entity.meshInvisible = true; // 隐身 ⬆ MC 扩展 | 获取/设置发光效果(类似光灵箭效果)。 ```js -entity.glowing = true; // 实体发光 +entity.glowing = true; // 实体发光 console.log(entity.glowing); ``` @@ -144,8 +136,6 @@ entity.nameTag = "§cBoss 怪物"; console.log(entity.nameTag); ``` ---- - ## 标签系统 全部 ✅ Box3 API。标签是附加在实体上的字符串标记,用于分类和查询。 @@ -167,15 +157,13 @@ entity.addTag("boss"); entity.addTag("red_team"); if (entity.hasTag("boss")) { - entity.maxHp = 200; + entity.maxHp = 200; } // 通过标签查询 var bosses = world.querySelectorAll(".boss"); ``` ---- - ## 火焰 ### entity.setFire(ticks) @@ -188,11 +176,9 @@ var bosses = world.querySelectorAll(".boss"); ```js entity.setFire(100); // 点燃 5 秒 -entity.clearFire(); // 立即扑灭 +entity.clearFire(); // 立即扑灭 ``` ---- - ## AI 与导航 ### entity.setAI(enabled) @@ -247,8 +233,6 @@ entity.lookAt(0, 100, -10); entity.lookAt(target.position); ``` ---- - ## 药水效果 全部 ⬆ MC 扩展。 @@ -262,9 +246,9 @@ entity.lookAt(target.position); 添加效果并可选隐藏粒子。 ```js -entity.addEffect("minecraft:speed", 600, 2); // 速度 III,30 秒 +entity.addEffect("minecraft:speed", 600, 2); // 速度 III,30 秒 entity.addEffect("minecraft:strength", 99999, 1, true); // 永久力量 II,不显示粒子 -entity.addEffect("minecraft:glowing", 200, 0); // 发光 10 秒 +entity.addEffect("minecraft:glowing", 200, 0); // 发光 10 秒 // 常用效果: // minecraft:speed, minecraft:slowness, minecraft:strength @@ -273,8 +257,6 @@ entity.addEffect("minecraft:glowing", 200, 0); // 发光 10 秒 // minecraft:glowing, minecraft:levitation, minecraft:fire_resistance ``` ---- - ## 装备 全部 ⬆ MC 扩展。 @@ -298,11 +280,9 @@ entity.setEquipment("feet", "minecraft:leather_boots"); ```js entity.setDropChance("mainhand", 0.5); // 50% 概率掉落主手物品 -entity.setDropChance("all", 0); // 不掉落任何装备 +entity.setDropChance("all", 0); // 不掉落任何装备 ``` ---- - ## 属性 全部 ⬆ MC 扩展。 @@ -326,8 +306,6 @@ entity.setAttribute("minecraft:generic.armor", 10); > 注意:`maxHp` / `walkSpeed` / `jumpPower` 等 Box3 便捷属性内部也使用这些 attribute,推荐优先使用便捷属性,仅当需要访问未封装的属性时才用 `setAttribute`。 ---- - ## 生命周期 ### entity.destroy() @@ -351,15 +329,16 @@ entity.setAttribute("minecraft:generic.armor", 10); ⬆ MC 扩展 | 设为 `true` 时生物不会因远离玩家而自然消失(仅 Mob 有效)。 ```js -var boss = world.spawnEntity("minecraft:wither_skeleton", new GameVector3(0, 100, 0)); +var boss = world.spawnEntity( + "minecraft:wither_skeleton", + new GameVector3(0, 100, 0), +); boss.setPersistent(true); // 不会消失 boss.setOnDestroy((e) => { - world.say("Boss 被击败了!"); + world.say("Boss 被击败了!"); }); ``` ---- - ## 自定义属性 ✅ Box3 API | 可以直接在 entity 上存储任意 JS 数据,存活期等于实体生命周期。 @@ -371,43 +350,3 @@ entity.killCount = 0; console.log(entity.myCustomField); ``` - ---- - -## Box3 API 列表 - -| API | 类型 | -|---|---| -| `id` | ✅ Box3 | -| `isPlayer()` | ✅ Box3 | -| `entityType` | ✅ Box3 | -| `position` | ✅ Box3 | -| `velocity` | ✅ Box3 | -| `bounds` | ✅ Box3 | -| `meshInvisible` | ✅ Box3 | -| `addTag()` / `hasTag()` / `removeTag()` | ✅ Box3 | -| `hp` / `maxHp` | ✅ Box3 | -| `hurt()` / `heal()` | ✅ Box3 | -| `destroy()` / `destroyed` | ✅ Box3 | -| `setOnDestroy()` | ✅ Box3 | - -## MC 扩展列表 - -| API | 类型 | -|---|---| -| `onGround` | ⬆ MC | -| `eyePosition` | ⬆ MC | -| `invulnerable` | ⬆ MC | -| `glowing` | ⬆ MC | -| `nameTag` | ⬆ MC | -| `setFire()` / `clearFire()` | ⬆ MC | -| `setAI()` | ⬆ MC | -| `setTarget()` / `getTarget()` / `clearTarget()` | ⬆ MC | -| `navigateTo()` | ⬆ MC | -| `lookAt()` | ⬆ MC | -| `addEffect()` | ⬆ MC | -| `setEquipment()` | ⬆ MC | -| `setDropChance()` | ⬆ MC | -| `getAttribute()` / `setAttribute()` | ⬆ MC | -| `remove()` | ⬆ MC | -| `setPersistent()` | ⬆ MC | 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 index 0ea018d6..82c65e13 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/math.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/math.md @@ -2,8 +2,6 @@ 全部 ✅ Box3 API。以下数据类型在 JS 中全局可用。 ---- - ## GameVector3 三维向量,用于位置、方向、速度等。 @@ -11,32 +9,32 @@ ### 构造 ```js -var v = new GameVector3(0, 100, 0); // x, y, z +var v = new GameVector3(0, 100, 0); // x, y, z ``` ### 属性 ```js -v.x = 10; // 读写 +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` | 分量相等比较 | +| `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` | 分量相等比较 | ### 静态方法 @@ -49,7 +47,7 @@ var pos = new GameVector3(0, 100, 0); var target = new GameVector3(10, 100, 10); // 计算两点距离 -var dist = pos.distance(target); // ~14.14 +var dist = pos.distance(target); // ~14.14 // 方向向量 var dir = target.sub(pos).normalize(); @@ -58,8 +56,6 @@ var dir = target.sub(pos).normalize(); entity.position.set(0, 100, 0); ``` ---- - ## GameBounds3 轴对齐包围盒(AABB)。 @@ -68,19 +64,17 @@ entity.position.set(0, 100, 0); ```js var bounds = new GameBounds3( - new GameVector3(-1, 0, -1), // 下界 (lo) - new GameVector3(1, 2, 1) // 上界 (hi) + new GameVector3(-1, 0, -1), // 下界 (lo) + new GameVector3(1, 2, 1), // 上界 (hi) ); ``` ### 方法 -| 方法 | 返回值 | 说明 | -|---|---|---| +| 方法 | 返回值 | 说明 | +| -------------------------- | --------- | -------------------- | | `bounds.intersects(other)` | `boolean` | 与另一包围盒是否相交 | -| `bounds.contains(point)` | `boolean` | 点是否在包围盒内 | - ---- +| `bounds.contains(point)` | `boolean` | 点是否在包围盒内 | ## GameRGBColor @@ -97,15 +91,15 @@ var gray = new GameRGBColor(0.5, 0.5, 0.5); ### 属性 ```js -color.r = 0.5; // 读写 +color.r = 0.5; // 读写 color.g = 0.8; color.b = 0.2; ``` ### 方法 -| 方法 | 返回值 | 说明 | -|---|---|---| +| 方法 | 返回值 | 说明 | +| -------------- | -------------- | -------- | | `c.lerp(d, t)` | `GameRGBColor` | 线性插值 | ### 静态方法 @@ -114,8 +108,6 @@ color.b = 0.2; var randomColor = GameRGBColor.random(); // 随机颜色 ``` ---- - ## GameRGBAColor 带 Alpha 通道的颜色,分量范围 0.0–1.0。 @@ -132,29 +124,27 @@ var semiRed = new GameRGBAColor(1, 0, 0, 0.5); 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); // 分量除法 +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.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); // 设置分量 +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(); // 深拷贝 +result.copy(a); // 浅拷贝 a +var clone = a.clone(); // 深拷贝 var lerped = a.lerp(b, 0.5); // 插值 -var eq = a.equals(b); // 比较 +var eq = a.equals(b); // 比较 ``` ---- - ## GameQuaternion 四元数,用于 3D 旋转。 @@ -167,21 +157,21 @@ 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)` | 比较 | +| 方法 | 说明 | +| ------------------------------------------------- | ------------------------- | +| `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)` | 比较 | ### 静态方法 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 index 5d62bff6..e128f061 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/player.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/player.md @@ -4,13 +4,11 @@ ```js world.onPlayerJoin((entity) => { - var p = entity.player; // p 即为 player 对象 - p.directMessage("欢迎回来, " + p.name + "!"); + var p = entity.player; // p 即为 player 对象 + p.directMessage("欢迎回来, " + p.name + "!"); }); ``` ---- - ## 基本信息 ### player.name @@ -27,13 +25,11 @@ world.onPlayerJoin((entity) => { ```js if (player.getOpLevel() >= 2) { - // 需要权限等级 2 的操作 + // 需要权限等级 2 的操作 } -player.opLevel = 3; // 设置为 3 级权限 +player.opLevel = 3; // 设置为 3 级权限 ``` ---- - ## 外观 ### player.invisible @@ -45,11 +41,9 @@ player.opLevel = 3; // 设置为 3 级权限 ✅ Box3 API | 只读。玩家缩放值。 ```js -player.invisible = true; // 隐形 +player.invisible = true; // 隐形 ``` ---- - ## 移动 全部 ✅ Box3 API。 @@ -75,18 +69,16 @@ player.invisible = true; // 隐形 只读。当前行走状态:`"CROUCH"`、`"RUN"`、`"WALK"`、`"NONE"`。 ```js -player.walkSpeed = 0.2; // 加速 -player.jumpPower = 0.6; // 跳更高 +player.walkSpeed = 0.2; // 加速 +player.jumpPower = 0.6; // 跳更高 world.onTick(() => { - if (player.walkState === "RUN") { - // 玩家在奔跑 - } + if (player.walkState === "RUN") { + // 玩家在奔跑 + } }); ``` ---- - ## 飞行 ### player.canFly @@ -126,11 +118,31 @@ player.disableFly = true; ⬆ MC 扩展 | 获取/设置团队内碰撞。设为 `false` 可防止多人推搡。底层修改团队的 `CollisionRule`。 ```js -player.collision = false; // 禁用碰撞 +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 封顶 +``` ## 游戏模式 @@ -139,15 +151,13 @@ console.log(player.collision); // false ✅ Box3 API | 获取/设置游戏模式。get 返回名称字符串,set 接受字符串或数字。 ```js -player.gameMode = "creative"; // 创造模式 -player.gameMode = "survival"; // 生存模式 -player.gameMode = "adventure"; // 冒险模式 -player.gameMode = "spectator"; // 旁观模式 +player.gameMode = "creative"; // 创造模式 +player.gameMode = "survival"; // 生存模式 +player.gameMode = "adventure"; // 冒险模式 +player.gameMode = "spectator"; // 旁观模式 // 或数字: 0=生存, 1=创造, 2=冒险, 3=旁观 ``` ---- - ## 相机 全部 ✅ Box3 API。 @@ -189,8 +199,6 @@ var dir = player.facingDirection; var target = player.cameraTarget; ``` ---- - ## 二段跳 全部 ⬆ MC 扩展。 @@ -210,7 +218,7 @@ var target = player.cameraTarget; ```js // 需要在 tick 中持续调用 world.onTick(() => { - player.doubleJump(); + player.doubleJump(); }); ``` @@ -218,11 +226,9 @@ world.onTick(() => { ```js player.canDoubleJump = true; -player.doubleJumpPower = 0.6; // 比普通跳跃高 +player.doubleJumpPower = 0.6; // 比普通跳跃高 ``` ---- - ## 传送与重生 ### player.teleport(pos) @@ -250,8 +256,6 @@ player.dimension = "minecraft:the_nether"; player.teleport(new GameVector3(0, 70, 0)); ``` ---- - ## 踢出 ### player.kick() @@ -266,8 +270,6 @@ player.teleport(new GameVector3(0, 70, 0)); player.kick("你已被移出游戏"); ``` ---- - ## 消息 ### player.directMessage(msg) @@ -292,8 +294,8 @@ player.kick("你已被移出游戏"); ```js var result = player.dialog({ - content: "选择你的道路", - options: ["战士", "法师", "弓箭手"] + content: "选择你的道路", + options: ["战士", "法师", "弓箭手"], }); player.directMessage("你选择了: " + result.value); ``` @@ -315,14 +317,12 @@ player.link("https://example.com"); // 对话树 player.directMessage("输入你的选择: A 或 B"); player.onChat((entity, msg, tick) => { - if (msg === "A") { - player.directMessage("你选择了 A"); - } + if (msg === "A") { + player.directMessage("你选择了 A"); + } }); ``` ---- - ## 经验与饱食度 ### player.xp @@ -342,14 +342,12 @@ player.onChat((entity, msg, tick) => { ⬆ MC 扩展 | 获取/设置饱和度(0–20,浮点数)。 ```js -player.xp = 10; // 设置 10 级 -player.addExperienceLevels(3); // 加 3 级 +player.xp = 10; // 设置 10 级 +player.addExperienceLevels(3); // 加 3 级 player.food = 20; player.saturation = 10; ``` ---- - ## 背包 全部 ⬆ MC 扩展。 @@ -383,15 +381,15 @@ player.clearInventory(); ```js player.giveEnchantedItem("minecraft:diamond_sword", 1, { - "minecraft:sharpness": 5, - "minecraft:fire_aspect": 2, - "minecraft:unbreaking": 3 + "minecraft:sharpness": 5, + "minecraft:fire_aspect": 2, + "minecraft:unbreaking": 3, }); player.giveEnchantedItem("minecraft:bow", 1, { - "minecraft:power": 5, - "minecraft:punch": 2, - "minecraft:infinity": 1 + "minecraft:power": 5, + "minecraft:punch": 2, + "minecraft:infinity": 1, }); ``` @@ -401,18 +399,16 @@ player.giveEnchantedItem("minecraft:bow", 1, { ```js player.giveNamedItem("minecraft:gold_ingot", 1, "§6§l跑酷金牌", [ - "§7天空跑酷锦标赛", - "§e完赛时间: 1:23.450" + "§7天空跑酷锦标赛", + "§e完赛时间: 1:23.450", ]); player.giveNamedItem("minecraft:diamond_sword", 1, "§c§l烈焰之刃", [ - "§7绑定: 火焰", - "§e右键: 发射火球" + "§7绑定: 火焰", + "§e右键: 发射火球", ]); ``` ---- - ## 药水效果 ### player.addEffect(effectId, duration, amplifier) @@ -433,8 +429,6 @@ player.addEffect("minecraft:jump_boost", 99999, 1, true); // 永久,无粒子 player.clearEffects(); ``` ---- - ## 音效与指令 ### player.playSound(path, volume, pitch) @@ -450,8 +444,6 @@ player.playSound("minecraft:block.note_block.pling", 0.8, 1.5); player.runCommand("say hello"); ``` ---- - ## Tab 列表 ### player.setPlayerListName(name) @@ -465,48 +457,3 @@ player.setPlayerListName("§6★ §f" + player.name); // 重置为原名 player.setPlayerListName(player.name); ``` - ---- - -## Box3 API 列表 - -| API | 类型 | -|---|---| -| `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 参) | ✅ Box3 | -| `dialog()` | ✅ Box3 | -| `link()` | ✅ Box3 | -| `onChat()` (player-level) | ✅ Box3 | - -## MC 扩展列表 - -| API | 类型 | -|---|---| -| `collision` | ⬆ MC | -| `title()` (5 参) | ⬆ MC | -| `xp` / `addExperienceLevels()` | ⬆ MC | -| `food` / `saturation` | ⬆ MC | -| `giveItem()` / `clearInventory()` / `getHeldItem()` | ⬆ MC | -| `giveEnchantedItem()` / `giveNamedItem()` | ⬆ MC | -| `addEffect()` (3/4 参) / `clearEffects()` | ⬆ MC | -| `playSound()` | ⬆ MC | -| `dimension` | ⬆ MC | -| `lookAt()` | ⬆ MC | -| `runCommand()` | ⬆ MC | -| `setPlayerListName()` | ⬆ MC | -| `getOpLevel()` / `opLevel` | ⬆ MC | -| `canDoubleJump` / `doubleJumpPower` / `doubleJump()` | ⬆ MC | 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..3bf8ca95 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/player_en.md @@ -0,0 +1,505 @@ +# 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; +``` + +## Double Jump + +All ⬆ MC Extension. + +### player.canDoubleJump + +Get/set whether double jump is allowed. When `true`, the player can jump once more in mid-air. + +### player.doubleJumpPower + +Vertical force of the double jump, default `0.42` (same as normal jump). Increase to jump higher. + +### player.doubleJump() + +Execute a double jump (must be called in a tick callback). Only works when `canDoubleJump = true`, the player is in the air, and hasn't used the double jump yet. Auto-resets on landing. + +```js +// Must be called continuously in a tick callback +world.onTick(() => { + player.doubleJump(); +}); +``` + +Typical usage — enable double jump in colorzone: + +```js +player.canDoubleJump = true; +player.doubleJumpPower = 0.6; // higher than normal jump +``` + +## 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 | +| `canDoubleJump` / `doubleJumpPower` / `doubleJump()` | ⬆ MC | diff --git a/Box3JS-NeoForge-1.21.1/docs/api/storage.md b/Box3JS-NeoForge-1.21.1/docs/api/storage.md index ff2e8389..00c58cce 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/storage.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/storage.md @@ -2,8 +2,6 @@ `storage` 提供 JSON 文件持久化存储,带内存缓存加速读写。数据保存在 `config/box3/storage/<项目名>/` 目录下,每个项目自动拥有独立命名空间。 ---- - ## 获取存储实例 ### storage.getDataStorage(name) @@ -19,8 +17,6 @@ var store = storage.getDataStorage("leaderboard"); var config = storage.getDataStorage("settings"); ``` ---- - ## 读写操作 ### store.set(key, value) @@ -36,12 +32,13 @@ 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) +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" @@ -54,12 +51,10 @@ var cfg = store.get("config"); // {difficulty: "hard", ...} (object ```js var keys = store.keys(); for (var i = 0; i < keys.length; i++) { - console.log(keys[i] + " = " + store.get(keys[i])); + console.log(keys[i] + " = " + store.get(keys[i])); } ``` ---- - ## 更新与删除 ### store.update(key, handler) @@ -68,8 +63,8 @@ for (var i = 0; i < keys.length; i++) { ```js store.set("counter", 0); -store.update("counter", function(current) { - return current + 1; // 原子递增 +store.update("counter", function (current) { + return current + 1; // 原子递增 }); ``` @@ -86,8 +81,6 @@ store.remove("tempKey"); store.destroy(); // 删除该存储的所有数据 ``` ---- - ## 数值操作 ### store.increment(key, delta) @@ -96,35 +89,33 @@ store.destroy(); // 删除该存储的所有数据 ```js store.set("kills", 0); -store.increment("kills"); // kills = 1 -store.increment("kills", 5); // kills = 6 +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"`) | +| 字段 | 类型 | 说明 | +| ------------------ | ------- | ----------------------------------- | +| `cursor` | number | 起始游标(页码 × pageSize) | +| `pageSize` | number | 每页条目数(1–100,默认 100) | +| `ascending` | boolean | 是否升序排列 | +| `max` | number | 值的上限过滤 | +| `min` | number | 值的下限过滤 | +| `constraintTarget` | string | 排序/过滤的嵌套路径(如 `"a.b.c"`) | 返回 `QueryList` 分页对象: -| 属性/方法 | 说明 | -|---|---| -| `result.isLastPage` | 是否最后一页 | +| 属性/方法 | 说明 | +| ------------------------- | ------------------ | +| `result.isLastPage` | 是否最后一页 | | `result.getCurrentPage()` | 返回当前页条目数组 | -| `result.nextPage()` | 移到下一页 | +| `result.nextPage()` | 移到下一页 | 每条条目为 `{key, value, updateTime, createTime, version}`。 @@ -134,17 +125,15 @@ 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); + console.log(page[i].key + ": " + page[i].value); } // 下一页 if (!result.isLastPage) { - result.nextPage(); + result.nextPage(); } ``` ---- - ## 内存缓存与持久化 所有 `GameDataStorage` 实例共享一个内存缓存(`ConcurrentHashMap`)。首次访问时从磁盘加载 JSON,后续读写均在内存中操作,每次写操作(`set`/`update`/`remove`/`increment`)同步刷盘。 @@ -153,8 +142,6 @@ if (!result.isLastPage) { - **项目隔离**:`getDataStorage("scores")` 在不同项目中访问不同文件(自动添加项目名前缀) - **跨项目共享**:`getGroupStorage("leaderboard")` 所有项目访问同一个 `__shared__/leaderboard.json` ---- - ## 完整示例:排行榜 ```js @@ -163,7 +150,7 @@ var lb = storage.getGroupStorage("leaderboard"); // 保存成绩 function saveScore(name, time) { - lb.set(name, time); + lb.set(name, time); } saveScore("Steve", 12345); @@ -172,15 +159,13 @@ 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(); + 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 index 0806d04e..2aa59c6a 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/voxels.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/voxels.md @@ -2,8 +2,6 @@ `voxels` 提供纯方块层面的读写操作。不涉及实体逻辑。 ---- - ## 方块信息 ### voxels.shape @@ -14,8 +12,6 @@ ✅ Box3 API | 只读字符串数组。所有已注册方块名称列表。 ---- - ## 名称 ↔ ID ### voxels.id(name) @@ -27,17 +23,16 @@ ✅ Box3 API | 内部 ID → 方块名称。 ```js -var stoneId = voxels.id("minecraft:stone"); // 获取 ID -var name = voxels.name(stoneId); // "minecraft:stone" +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 编码) @@ -84,14 +79,20 @@ voxels.setVoxel(new GameVector3(0, 100, 0), "minecraft:oak_stairs", 2); ```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(-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.fillVoxel( + new GameVector3(-5, 100, -5), + new GameVector3(5, 110, 5), + "minecraft:air", +); ``` ---- - ## 读取方块 ### voxels.getVoxel(x, y, z) @@ -127,9 +128,9 @@ voxels.fillVoxel(new GameVector3(-5, 100, -5), new GameVector3(5, 110, 5), "mine ⬆ 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 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 重载 @@ -147,12 +148,22 @@ var name = voxels.getVoxelName(new GameVector3(0, 100, 0)); ```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"); +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) @@ -168,47 +179,3 @@ voxels.setVoxel(0, 100, 0, "minecraft:spawner"); voxels.setSpawner(0, 100, 0, "minecraft:zombie"); voxels.setSpawner(new GameVector3(0, 100, 0), "minecraft:skeleton"); ``` - ---- - -## 常用方块 ID 参考 - -```js -// 建筑方块 -"minecraft:stone" "minecraft:stone_bricks" "minecraft:cobblestone" -"minecraft:glass" "minecraft:white_concrete" "minecraft:oak_planks" -"minecraft:obsidian" "minecraft:bedrock" "minecraft:gold_block" -"minecraft:diamond_block" "minecraft:beacon" - -// 装饰 -"minecraft:sea_lantern" "minecraft:glowstone" -"minecraft:white_stained_glass" "minecraft:cyan_terracotta" - -// 特殊 -"minecraft:air" // 清除方块 -"minecraft:spawner" // 刷怪笼 -"minecraft:water" // 水 -"minecraft:lava" // 岩浆 -``` - ---- - -## Box3 API 列表 - -| API | 类型 | -|---|---| -| `shape` | ✅ Box3 | -| `VoxelTypes` | ✅ Box3 | -| `id()` / `name()` | ✅ Box3 | -| `setVoxel()` | ✅ Box3 | -| `setVoxelId()` | ✅ Box3 | -| `getVoxel()` / `getVoxelId()` / `getVoxelName()` | ✅ Box3 | -| `getVoxelRotation()` | ✅ Box3 | - -## MC 扩展列表 - -| API | 类型 | -|---|---| -| `fillVoxel()` | ⬆ MC | -| `countVoxel()` | ⬆ MC | -| `setSpawner()` | ⬆ MC | 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 index 3b5a12eb..61e47fae 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/world.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/world.md @@ -2,8 +2,6 @@ `world` 是全局单例,代表 Minecraft 服务端的世界状态。控制天气、时间、游戏规则、实体生成,注册事件回调,管理记分板/Bossbar/队伍,以及发射粒子、烟花、闪电等视觉效果。 ---- - ## 世界属性 ### world.projectName() @@ -23,8 +21,6 @@ var uptime = world.currentTick(); world.say("服务器已运行 " + Math.floor(uptime / 20 / 60) + " 分钟"); ``` ---- - ## 天气 ### world.rainDensity @@ -32,7 +28,7 @@ world.say("服务器已运行 " + Math.floor(uptime / 20 / 60) + " 分钟"); ✅ Box3 API | 获取/设置降雨强度,范围 0.0–1.0。 ```js -world.rainDensity = 1.0; // 满强度下雨 +world.rainDensity = 1.0; // 满强度下雨 console.log(world.rainDensity); // 0.0 ~ 1.0 ``` @@ -52,8 +48,6 @@ world.thunderDensity = 0.5; world.clearWeather(); ``` ---- - ## 时间 ### world.time @@ -61,7 +55,7 @@ world.clearWeather(); ✅ Box3 API | 获取/设置世界时间(tick)。Minecraft 一天 = 24000 tick。 ```js -world.time = 6000; // 正午 +world.time = 6000; // 正午 world.time = 18000; // 午夜 console.log(world.time); // 当前时间 ``` @@ -69,7 +63,7 @@ console.log(world.time); // 当前时间 此外提供 `world.setTime(tick)` 方法作为便捷设置接口。 ```js -world.setTime(6000); // 等效于 world.time = 6000 +world.setTime(6000); // 等效于 world.time = 6000 ``` 常用时间值:`0` 日出、`6000` 正午、`12000` 日落、`18000` 午夜。 @@ -79,12 +73,10 @@ world.setTime(6000); // 等效于 world.time = 6000 ✅ Box3 API | 获取/设置时间流速。`0` = 暂停,`1` = 正常。底层修改 `doDaylightCycle` 游戏规则。 ```js -world.timeScale = 0; // 冻结时间 -world.timeScale = 1; // 恢复正常 +world.timeScale = 0; // 冻结时间 +world.timeScale = 1; // 恢复正常 ``` ---- - ## 难度 ### world.difficulty @@ -93,14 +85,12 @@ world.timeScale = 1; // 恢复正常 ```js world.difficulty = "hard"; -world.difficulty = 3; // 等同 hard +world.difficulty = 3; // 等同 hard console.log(world.difficulty); // "hard" // 有效值: "peaceful"(0), "easy"(1), "normal"(2), "hard"(3) ``` ---- - ## 出生点 ### world.spawnPoint @@ -115,8 +105,6 @@ console.log(world.difficulty); // "hard" world.setWorldSpawn(new GameVector3(0, 70, 0)); ``` ---- - ## 游戏规则 ### world.getGameRule(name) @@ -129,15 +117,15 @@ world.setWorldSpawn(new GameVector3(0, 70, 0)); **支持的规则:** -| 规则名 | 说明 | -|---|---| -| `doDaylightCycle` | 时间流动 | -| `doWeatherCycle` | 天气变化 | -| `keepInventory` | 死亡不掉落 | -| `doMobSpawning` | 生物自然生成 | -| `doFireTick` | 火焰蔓延 | -| `mobGriefing` | 生物破坏方块 | -| `doImmediateRespawn` | 立即重生 | +| 规则名 | 说明 | +| -------------------- | ------------ | +| `doDaylightCycle` | 时间流动 | +| `doWeatherCycle` | 天气变化 | +| `keepInventory` | 死亡不掉落 | +| `doMobSpawning` | 生物自然生成 | +| `doFireTick` | 火焰蔓延 | +| `mobGriefing` | 生物破坏方块 | +| `doImmediateRespawn` | 立即重生 | ```js world.setGameRule("keepInventory", true); @@ -145,8 +133,6 @@ world.setGameRule("doFireTick", false); console.log(world.getGameRule("doMobSpawning")); // true/false ``` ---- - ## 实体生成 ### world.spawnEntity(type, pos) @@ -162,60 +148,56 @@ 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()` 消息 | +| 事件 | 类型 | 回调签名 | 触发时机 | +| ---------------------------- | ------- | ------------------------------------------------------ | ------------------------------- | +| `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 执行 + // 每 tick 执行 }); world.onPlayerJoin((entity) => { - var p = entity.player; - world.say(p.name + " 加入了游戏"); - p.teleport(new GameVector3(0, 100, 0)); + 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)); - } + 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); - } + if (killer && killer.isPlayer()) { + var kp = killer.player; + kp.addExperienceLevels(1); + } }); ``` ---- - ## 查询 ### world.querySelectorAll(selector) @@ -228,22 +210,22 @@ world.onEntityDeath((entity, killer) => { **选择器语法:** -| 选择器 | 含义 | -|---|---| -| `"*"` | 所有在线玩家 | -| `"#uuid"` | 按 UUID 精确匹配 | -| `".tagName"` | 按标签匹配 | +| 选择器 | 含义 | +| ------------ | ---------------- | +| `"*"` | 所有在线玩家 | +| `"#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 p = allPlayers[i].player; + p.actionBar("在线人数: " + allPlayers.length); } var specific = world.querySelector("#550e8400-e29b-41d4-a716-446655440000"); if (specific) { - specific.player.directMessage("找到你了"); + specific.player.directMessage("找到你了"); } ``` @@ -255,8 +237,6 @@ if (specific) { world.say("§6[公告] §f比赛即将开始!"); ``` ---- - ## 计时器 ### world.setTimeout(handler, ticks) @@ -277,11 +257,11 @@ world.say("§6[公告] §f比赛即将开始!"); ```js var tid = world.setTimeout(() => { - world.say("3 秒后执行"); + world.say("3 秒后执行"); }, 60); // 60 ticks = 3 秒 var iid = world.setInterval(() => { - world.say("每 10 秒执行一次"); + world.say("每 10 秒执行一次"); }, 200); // 200 ticks = 10 秒 // 取消 @@ -289,8 +269,6 @@ world.clearTimeout(tid); world.clearInterval(iid); ``` ---- - ## 记分板 全部 ⬆ MC 扩展。 @@ -339,8 +317,6 @@ world.hideScoreboard("sidebar"); world.removeScoreboard("kills"); ``` ---- - ## Boss 血条 全部 ⬆ MC 扩展。 @@ -349,12 +325,12 @@ world.removeScoreboard("kills"); 显示或更新 Boss 血条。 -| 参数 | 说明 | -|---|---| -| `name` | 血条 ID,用于后续更新或移除 | -| `text` | 显示文本(支持颜色代码) | -| `progress` | 0.0–1.0,进度条长度 | -| `color` | `"blue"`、`"green"`、`"pink"`、`"purple"`、`"red"`、`"white"`、`"yellow"` | +| 参数 | 说明 | +| ---------- | ------------------------------------------------------------------------- | +| `name` | 血条 ID,用于后续更新或移除 | +| `text` | 显示文本(支持颜色代码) | +| `progress` | 0.0–1.0,进度条长度 | +| `color` | `"blue"`、`"green"`、`"pink"`、`"purple"`、`"red"`、`"white"`、`"yellow"` | ### world.removeBossbar(name) @@ -364,22 +340,22 @@ world.removeScoreboard("kills"); // 创建一个 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"); - } + 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 扩展。 @@ -409,14 +385,12 @@ 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"); + // 交替分边 + var online = world.querySelectorAll("*").length; + world.joinTeam(entity, online % 2 === 0 ? "red_team" : "blue_team"); }); ``` ---- - ## 世界边界 全部 ⬆ MC 扩展。 @@ -449,12 +423,10 @@ world.setBorderDamage(2); world.setBorderWarning(10); world.setTimeout(() => { - world.shrinkBorder(100, 120); // 2 分钟缩到 100 + world.shrinkBorder(100, 120); // 2 分钟缩到 100 }, 600); // 30 秒后开始 ``` ---- - ## 视觉效果 全部 ⬆ MC 扩展。 @@ -521,7 +493,12 @@ 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); +world.spawnParticleCircle( + new GameVector3(0, 100, 0), + 2.0, + "minecraft:happy_villager", + 20, +); // 常用粒子: // minecraft:flame, minecraft:cloud, minecraft:happy_villager @@ -529,8 +506,6 @@ world.spawnParticleCircle(new GameVector3(0, 100, 0), 2.0, "minecraft:happy_vill // minecraft:heart, minecraft:note, minecraft:dragon_breath ``` ---- - ## 物品 / 抛射物 全部 ⬆ MC 扩展。 @@ -559,14 +534,17 @@ world.dropItem(entity.position, "minecraft:diamond", 3); ```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:fireball", + new GameVector3(0, 100, 0), + new GameVector3(10, 100, 10), + 2, +); // 发射箭 world.launchProjectile("minecraft:arrow", 0, 100, 0, 5, 105, 0, 3); ``` ---- - ## 爆炸 / 音效 / 查询 全部 ⬆ MC 扩展。 @@ -588,7 +566,7 @@ world.launchProjectile("minecraft:arrow", 0, 100, 0, 5, 105, 0, 3); ⬆ GameVector3 重载。 ```js -world.explode(0, 100, 0, 4); // 威力 4,不引火 +world.explode(0, 100, 0, 4); // 威力 4,不引火 world.explode(new GameVector3(0, 100, 0), 8, true); // 威力 8,引火 ``` @@ -602,7 +580,12 @@ world.explode(new GameVector3(0, 100, 0), 8, true); // 威力 8,引火 ```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.playSound( + "minecraft:block.note_block.pling", + new GameVector3(0, 100, 0), + 1.0, + 1.5, +); ``` ### world.raycast(origin, direction) @@ -619,10 +602,10 @@ world.playSound("minecraft:block.note_block.pling", new GameVector3(0, 100, 0), 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); - } + console.log("命中方块:", result.voxel, "距离:", result.distance); + if (result.entity) { + console.log("命中实体:", result.entity.entityType); + } } ``` @@ -643,7 +626,7 @@ if (result.hit) { 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); + console.log(nearby[i].entityType); } ``` @@ -661,8 +644,6 @@ console.log(biome); // "minecraft:plains" var biome = world.getBiome(entity.position); ``` ---- - ## 跨脚本消息 ### world.sendMessage(target, data) @@ -677,63 +658,3 @@ var biome = world.getBiome(entity.position); world.runCommand("time set day"); world.runCommand("weather clear"); ``` - ---- - -## Box3 API 列表 - -| API | 类型 | -|---|---| -| `currentTick()` | ✅ Box3 | -| `rainDensity` | ✅ Box3 | -| `time` / `setTime()` | ✅ Box3 | -| `timeScale` | ✅ Box3 | -| `difficulty` | ✅ Box3 | -| `spawnEntity()` | ✅ Box3 | -| `onTick()` | ✅ Box3 | -| `onPlayerJoin()` | ✅ Box3 | -| `onPlayerLeave()` | ✅ Box3 | -| `onVoxelDestroy()` | ✅ Box3 | -| `onVoxelContact()` | ✅ Box3 | -| `onInteract()` | ✅ Box3 | -| `onChat()` | ✅ Box3 | -| `onFluidEnter()` | ✅ Box3 | -| `onFluidLeave()` | ✅ Box3 | -| `onEntityContact()` | ✅ Box3 | -| `onEntitySeparate()` | ✅ Box3 | -| `querySelectorAll()` | ✅ Box3 | -| `querySelector()` | ✅ Box3 | -| `say()` | ✅ Box3 | - -## MC 扩展列表 - -| API | 类型 | -|---|---| -| `thunderDensity` | ⬆ MC | -| `clearWeather()` | ⬆ MC | -| `spawnPoint` / `setWorldSpawn()` | ⬆ MC | -| `getGameRule()` / `setGameRule()` | ⬆ MC | -| `onBlockPlace()` | ⬆ MC | -| `onEntityDeath()` | ⬆ MC | -| `onPlayerRespawn()` | ⬆ MC | -| `onBlockActivate()` | ⬆ MC | -| `onEntityDamage()` | ⬆ MC | -| `onMessage()` | ⬆ MC | -| `setTimeout()` / `setInterval()` / `clearTimeout()` / `clearInterval()` | ⬆ MC | -| `addScoreboard()` / `removeScoreboard()` / `setScore()` / `getScore()` | ⬆ MC | -| `showScoreboard()` / `hideScoreboard()` / `listScores()` | ⬆ MC | -| `showBossbar()` / `removeBossbar()` | ⬆ MC | -| `createTeam()` / `removeTeam()` / `joinTeam()` / `leaveTeam()` / `getTeamOf()` | ⬆ MC | -| `borderSize` / `setBorderCenter()` / `shrinkBorder()` | ⬆ MC | -| `setBorderDamage()` / `setBorderWarning()` | ⬆ MC | -| `strikeLightning()` | ⬆ MC | -| `launchFirework()` | ⬆ MC | -| `spawnParticle()` / `spawnParticleCircle()` | ⬆ MC | -| `dropItem()` | ⬆ MC | -| `launchProjectile()` | ⬆ MC | -| `explode()` | ⬆ MC | -| `playSound()` | ⬆ MC | -| `raycast()` | ⬆ MC | -| `entitiesInArea()` / `entitiesInRadius()` | ⬆ MC | -| `getBiome()` | ⬆ MC | -| `sendMessage()` / `runCommand()` | ⬆ MC | 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/src/main/resources/assets/box3js/template/build.mjs b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/build.mjs index 1b9ed241..7857bebe 100644 --- 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 @@ -13,23 +13,31 @@ import { } 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); @@ -40,6 +48,7 @@ function collectTsFiles(dir, out = []) { return out; } +// Step 1: Babel transpile TS -> temp JS / 第一步:使用 Babel 将 TS 转为临时 JS async function transpileTsToTemp() { cleanupTempDir(); mkdirSync(tempDir, { recursive: true }); @@ -55,7 +64,7 @@ async function transpileTsToTemp() { "@babel/preset-env", { targets: { rhino: "1.9.1" }, - modules: "commonjs", + modules: false, bugfixes: true, loose: true, }, @@ -73,6 +82,7 @@ async function transpileTsToTemp() { } } +// Step 2: Bundle and sanitize regex literal for Rhino parser / 第二步:打包并修正 Rhino 不支持的 helper 正则字面量 async function bundleAndSanitize() { await esbuild.build({ entryPoints: [resolve(tempDir, "app.js")], @@ -91,23 +101,27 @@ async function bundleAndSanitize() { 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; From 5cd3816f5a626728e87e83593c368b3acd5fd613 Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Wed, 6 May 2026 19:54:16 +0800 Subject: [PATCH 17/17] =?UTF-8?q?docs(readme):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=90=8D=E7=A7=B0=E5=92=8C=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 从项目标题中移除 "Shendao Code" - 将命令示例从 "/box3script reload mygame" 更新为 "/box3script on mygame" - 明确说明 Box3JS 受 Box3 编程风格启发而非完全遵循 feat(player): 移除二段跳功能 - 移除二段跳相关 API:canDoubleJump、doubleJumpPower 和 doubleJump() - 清理中英文 API 文档中的相关说明 - 移除 Box3JSPlayer.java 中的实现代码 - 更新 README 功能列表,移除二段跳相关内容 --- Box3JS-NeoForge-1.21.1/README_en.md | 6 ++-- Box3JS-NeoForge-1.21.1/docs/api/player.md | 30 ------------------ Box3JS-NeoForge-1.21.1/docs/api/player_en.md | 31 ------------------- .../box3lab/box3js/script/Box3JSPlayer.java | 20 ------------ .../assets/box3js/template/types/globals.d.ts | 21 ------------- README.md | 2 +- 6 files changed, 4 insertions(+), 106 deletions(-) diff --git a/Box3JS-NeoForge-1.21.1/README_en.md b/Box3JS-NeoForge-1.21.1/README_en.md index e6a3eac9..d765696e 100644 --- a/Box3JS-NeoForge-1.21.1/README_en.md +++ b/Box3JS-NeoForge-1.21.1/README_en.md @@ -1,10 +1,10 @@ -# Box3JS (Shendao Code) -- Minecraft Mod +# 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 that follows the coding style of Box3. You do not need to write Java — just use TypeScript to build scripts. +`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 @@ -47,7 +47,7 @@ npm run build # outputs dist/app.js Back in game and enable it: ``` -/box3script reload mygame +/box3script on mygame ``` ## Available APIs diff --git a/Box3JS-NeoForge-1.21.1/docs/api/player.md b/Box3JS-NeoForge-1.21.1/docs/api/player.md index e128f061..6dd37417 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/player.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/player.md @@ -199,36 +199,6 @@ var dir = player.facingDirection; var target = player.cameraTarget; ``` -## 二段跳 - -全部 ⬆ MC 扩展。 - -### player.canDoubleJump - -获取/设置是否允许二段跳。设为 `true` 后玩家在空中可再跳一次。 - -### player.doubleJumpPower - -二段跳的垂直力度,默认 `0.42`(与普通跳跃一致)。调大可以跳得更高。 - -### player.doubleJump() - -执行二段跳(需在 tick 回调中调用)。仅在 `canDoubleJump = true` 且玩家在空中、且本跳未使用过时才生效。落地自动重置。 - -```js -// 需要在 tick 中持续调用 -world.onTick(() => { - player.doubleJump(); -}); -``` - -典型用法 — 在 colorzone 中启用二段跳: - -```js -player.canDoubleJump = true; -player.doubleJumpPower = 0.6; // 比普通跳跃高 -``` - ## 传送与重生 ### player.teleport(pos) diff --git a/Box3JS-NeoForge-1.21.1/docs/api/player_en.md b/Box3JS-NeoForge-1.21.1/docs/api/player_en.md index 3bf8ca95..431729b3 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/player_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/player_en.md @@ -201,36 +201,6 @@ var dir = player.facingDirection; var target = player.cameraTarget; ``` -## Double Jump - -All ⬆ MC Extension. - -### player.canDoubleJump - -Get/set whether double jump is allowed. When `true`, the player can jump once more in mid-air. - -### player.doubleJumpPower - -Vertical force of the double jump, default `0.42` (same as normal jump). Increase to jump higher. - -### player.doubleJump() - -Execute a double jump (must be called in a tick callback). Only works when `canDoubleJump = true`, the player is in the air, and hasn't used the double jump yet. Auto-resets on landing. - -```js -// Must be called continuously in a tick callback -world.onTick(() => { - player.doubleJump(); -}); -``` - -Typical usage — enable double jump in colorzone: - -```js -player.canDoubleJump = true; -player.doubleJumpPower = 0.6; // higher than normal jump -``` - ## Teleport & Respawn ### player.teleport(pos) @@ -502,4 +472,3 @@ player.setPlayerListName(player.name); | `runCommand()` | ⬆ MC | | `setPlayerListName()` | ⬆ MC | | `getOpLevel()` / `opLevel` | ⬆ MC | -| `canDoubleJump` / `doubleJumpPower` / `doubleJump()` | ⬆ MC | 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 index b3b2bc3c..6a92e151 100644 --- 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 @@ -437,26 +437,6 @@ public void playSound(String path, double volume, double pitch) { } } - // ---- Double Jump ---- - - public boolean getCanDoubleJump() { return getProp("canDoubleJump", false); } - public void setCanDoubleJump(boolean v) { trackIfSandboxed(); setProp("canDoubleJump", v); } - - public double getDoubleJumpPower() { return getProp("doubleJumpPower", 0.42); } - public void setDoubleJumpPower(double v) { setProp("doubleJumpPower", v); } - - public void doubleJump() { - if (player.onGround()) { - setProp("hasDoubleJumped", false); - } - if (getCanDoubleJump() && !getProp("hasDoubleJumped", false) && !player.onGround()) { - double power = getDoubleJumpPower(); - player.setDeltaMovement(player.getDeltaMovement().x, power, player.getDeltaMovement().z); - player.hurtMarked = true; - setProp("hasDoubleJumped", true); - } - } - // ---- Custom properties ---- private void trackIfSandboxed() { 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 index f99f732d..ca416e00 100644 --- 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 @@ -884,27 +884,6 @@ interface GamePlayer { */ jumpPower: number; - /** - * 是否允许二段跳。 - * Whether double‑jump is enabled for this player. - */ - canDoubleJump: boolean; - - /** - * 二段跳力度 (默认 0.42, 等同于普通跳跃)。 - * Double‑jump power (default 0.42, same as a normal jump). - */ - doubleJumpPower: number; - - /** - * 执行二段跳 — 仅在玩家离地且 canDoubleJump 为 true 时生效。 - * 每次落地后自动重置, 同一滞空时间只能二段跳一次。 - * - * Performs a double jump — only works when the player is off the ground - * and canDoubleJump is true. Resets automatically on landing. - */ - doubleJump(): void; - /** * 当前移动状态。 * Current movement state. diff --git a/README.md b/README.md index e8d2eee8..36cf3a02 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ npm install && npm run build # 安装依赖并编译 - `world` — 世界控制、事件回调 (16 种)、记分板、Bossbar、队伍、边界、粒子、烟花、射线检测 - `entity` — 实体属性、AI 寻路、装备、药水效果、标签 -- `player` — 玩家专属:背包、飞行、游戏模式、二段跳、传送、消息、经验 +- `player` — 玩家专属:背包、飞行、游戏模式、传送、消息、经验 - `voxels` — 方块读写、区域填充、刷怪笼 - `storage` — JSON 数据持久化 - `console` / `require()` / `sleep()` / `GameVector3` / `GameBounds3` / `GameRGBColor` 等