Start working at 1.2.0
/ build (push) Has been cancelled

This commit is contained in:
2026-08-26 09:54:39 +08:00 Unverified
parent 36f2dd0685
commit 136d5ffa3b
159 changed files with 60793 additions and 617 deletions
+2
View File
@@ -4,3 +4,5 @@
docs/superpowers docs/superpowers
.claude .claude
node_modules/ node_modules/
.worktrees
__pycache__/
+27
View File
@@ -64,6 +64,33 @@
"echo": true, "echo": true,
"close": true "close": true
} }
},
{
"label": "bake: block models",
"type": "shell",
"command": "${command:python.interpreterPath}",
"args": [
"tools/bake_block_models/main.py"
],
"problemMatcher": [],
"presentation": {
"echo": true,
"reveal": "always",
"panel": "new",
"close": false
}
},
{
"label": "regolith: release",
"type": "shell",
"command": "regolith run release",
"problemMatcher": [],
"presentation": {
"echo": true,
"reveal": "always",
"panel": "new",
"close": false
}
} }
] ]
} }
+21 -8
View File
@@ -13,18 +13,19 @@
--- ---
Give yourself the tools to ease the survival building process with Construct, an addon designed to help you transfer your builds from creative to survival. Construct offers these convenient features and several more: Ease the survival building process with Construct, an addon designed to help you transfer your builds from creative to survival. Construct offers these convenient features and more:
- **Easy Place**: Always places blocks correctly. - **Structure Hologram**
- **Block Validation**: Highlights incorrect blocks. - **Block Validation**: Highlights incorrect blocks.
- **Easy Place**: Always places blocks correctly.
- **Material Grabbing**: Pulls required materials from chests in just one click. - **Material Grabbing**: Pulls required materials from chests in just one click.
- **Layered Building**: Build structures in layers. - **Layered Building**: Build structures in layers.
- **Structure Management**: Create and edit many structures at once. - **Structure Management**: Create and edit many structures at once.
- **Structure Importing**: Import structures from other worlds quickly and easily. - **Structure Importing**: Import structures from other worlds quickly and easily.
<div align="center"> <div align="center">
<a href="./demo.png"> <a href="./demo.gif">
<img src="./demo.png" alt="Construct Structure Demo" height="350"> <img src="./demo.gif" alt="Construct Structure Demo" height="350">
</a> </a>
</div> </div>
@@ -49,7 +50,14 @@ Construct uses Minecraft's vanilla structure system so that you can easily creat
1. Move the `Construct[BP]` to your `com.mojang/development_behavior_packs` folder. 1. Move the `Construct[BP]` to your `com.mojang/development_behavior_packs` folder.
2. Drop your `.mcstructure` file in Construct's `Construct[BP]/structures` folder. 2. Drop your `.mcstructure` file in Construct's `Construct[BP]/structures` folder.
3. Restart your world. 3. Restart your world.
4. Your structure should now be in in Construct's structure list! 4. Your structure should now be in Construct's structure list!
- For devices that cannot export .mcstructure files:
1. Export your world file.
2. Go to https://holoprint-mc.github.io/ and use its "Extract From World" feature to get your .mcstructures.
- For devices that cannot manage Minecraft's files:
1. Go to https://sea-of-stars-studio.github.io/.
2. Upload your .mcstructure and generate a .mcpack with your structure inside.
3. Add the generated pack to the same world as Construct.
### Commands ### Commands
@@ -70,7 +78,10 @@ Need help, want to discuss technical Minecraft, or follow future updates? [**Joi
- [x] Material list - [x] Material list
- [x] Flexible structure movement - [x] Flexible structure movement
- [x] Translation support - [x] Translation support
- [ ] Block texture display - [x] Block texture display
- [x] Pick structure block
- [ ] Mirroring and Rotation
- [ ] Simpler Structure Import/Export
## Issues & Suggestions ## Issues & Suggestions
@@ -80,6 +91,8 @@ If you have any issues or suggestions, please don't hesitate to open an issue on
Construct currently supports American English and Chinese (thanks to [wed150](https://github.com/wed150) & [EndrTrekker](https://github.com/EndrTrekker)). If you would like to contribute a translation, please join our Discord and reach out! Construct currently supports American English and Chinese (thanks to [wed150](https://github.com/wed150) & [EndrTrekker](https://github.com/EndrTrekker)). If you would like to contribute a translation, please join our Discord and reach out!
### Donate ---
If you appreciate my work here and would like to support the future development of my addons, please consider donating to me on [BuyMeACoffee](https://buymeacoffee.com/forestoflight). Your support is greatly appreciated! I'm constantly working on improvements for Construct. If you would like to show a token of appreciation for my work and support the future development of my addons, please consider [buying me a coffee](https://buymeacoffee.com/forestoflight).
[![BuyMeACoffee](https://raw.githubusercontent.com/pachadotdev/buymeacoffee-badges/main/bmc-donate-yellow.svg)](https://buymeacoffee.com/forestoflight)
+23 -1
View File
@@ -8,7 +8,16 @@
}, },
"regolith": { "regolith": {
"dataPath": "./packs/data", "dataPath": "./packs/data",
"filterDefinitions": {}, "filterDefinitions": {
"fetch_block_data": {
"runWith": "python",
"script": "./filters/fetch_block_data/main.py"
},
"fetch_block_models": {
"runWith": "python",
"script": "./tools/bake_block_models/main.py"
}
},
"formatVersion": "1.4.0", "formatVersion": "1.4.0",
"profiles": { "profiles": {
"default": { "default": {
@@ -30,6 +39,19 @@
"bpName": "project.name+'[BP]'" "bpName": "project.name+'[BP]'"
}, },
"filters": [] "filters": []
},
"release": {
"export": {
"build": "standard",
"readOnly": false,
"target": "local",
"rpName": "project.name+'[RP]'",
"bpName": "project.name+'[BP]'"
},
"filters": [
{ "filter": "fetch_block_data" },
{ "filter": "fetch_block_models" }
]
} }
} }
} }
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 MiB

+2
View File
@@ -14,3 +14,5 @@ Detailed information about the available API endpoints can be found in the follo
- [Endpoints](./API/Endpoints.md) - [Endpoints](./API/Endpoints.md)
- [Data Models](./API/DataModels.md) - [Data Models](./API/DataModels.md)
Construct's API will follow semantic versioning, and any breaking changes will be documented in the release notes.
+7 -2
View File
@@ -37,10 +37,13 @@ interface Instance {
isEnabled: PROTO.Boolean, isEnabled: PROTO.Boolean,
trackPlayerDistance: PROTO.Int8, trackPlayerDistance: PROTO.Int8,
particleLifetime: PROTO.Int32 particleLifetime: PROTO.Int32
}) }),
renderMode: PROTO.String
} }
``` ```
`renderMode` is one of `"default"`, `"performance"`, `"previewOnly"`, or `"classic"`. Any other value is treated as `"default"`.
## Builder ## Builder
A `Builder` represents a single builder (Construct's name for Players) in the world, along with its settings and properties. A `Builder` represents a single builder (Construct's name for Players) in the world, along with its settings and properties.
@@ -51,6 +54,8 @@ interface Builder {
easyPlace: PROTO.Boolean, easyPlace: PROTO.Boolean,
fastEasyPlace: PROTO.Boolean, fastEasyPlace: PROTO.Boolean,
materialGrabber: PROTO.Boolean, materialGrabber: PROTO.Boolean,
materialInstanceName: PROTO.String materialInstanceName: PROTO.String,
defaultRenderMode: PROTO.String
} }
``` ```
`defaultRenderMode` is one of `"default"`, `"performance"`, `"previewOnly"`, or `"classic"`. Any other value is treated as `"default"`.
+7 -13
View File
@@ -12,7 +12,7 @@ Create a new instance bound to a structure.
| `<instanceName>` | Unique name for this instance. | | `<instanceName>` | Unique name for this instance. |
| `<structureId>` | ID of a structure saved in the world (without the `mystructure:` prefix). | | `<structureId>` | ID of a structure saved in the world (without the `mystructure:` prefix). |
> Corresponds to the "create new instance" flow in the main menu. Errors if `instanceName` is already taken or `structureId` does not exist. > Errors if `instanceName` is already taken or `structureId` does not exist.
### `construct:delete <instanceName>` ### `construct:delete <instanceName>`
Permanently delete an instance. Permanently delete an instance.
@@ -21,8 +21,6 @@ Permanently delete an instance.
|---|---| |---|---|
| `<instanceName>` | Name of the instance to delete. | | `<instanceName>` | Name of the instance to delete. |
> Calls `structureCollection.delete()`. Also disables the instance and clears its saved dynamic properties before removal.
### `construct:rename <instanceName> <newInstanceName>` ### `construct:rename <instanceName> <newInstanceName>`
Rename an existing instance. Rename an existing instance.
@@ -31,10 +29,10 @@ Rename an existing instance.
| `<instanceName>` | Current instance name. | | `<instanceName>` | Current instance name. |
| `<newInstanceName>` | Desired new name. Errors if already in use. | | `<newInstanceName>` | Desired new name. Errors if already in use. |
### `construct:list` ### `construct:instances`
List all registered instances and their status. List all registered instances and their status.
> Prints each instance name, its bound structure ID, enabled/disabled state, and placed location (if any). Useful for scripting and quick inspection without opening the GUI. > Prints each instance name, its bound structure ID, enabled/disabled state, and placed location (if any).
## Placement & Movement ## Placement & Movement
@@ -46,7 +44,7 @@ Enable and place an instance at a location.
| `<instanceName>` | Instance to place. | | `<instanceName>` | Instance to place. |
| `<x y z>` | World coordinates. Errors if omitted. Supports tilde (`~`) notation. | | `<x y z>` | World coordinates. Errors if omitted. Supports tilde (`~`) notation. |
> Equivalent to the "Place" button in the instance menu — enables the instance and calls `move()` in one step. If the instance already has a location, this is a move, not a fresh place. > If the instance already has a location, this is a move, not a fresh place.
### `construct:move <instanceName> [x y z]` ### `construct:move <instanceName> [x y z]`
Reposition a placed instance without toggling its enabled state. Reposition a placed instance without toggling its enabled state.
@@ -83,7 +81,7 @@ Set the active layer of an instance.
### `construct:nextlayer <instanceName>` ### `construct:nextlayer <instanceName>`
Step the layer up by one (wraps from max back to `0`). Step the layer up by one (wraps from max back to `0`).
> Mirrors the "Next layer" button. Wrapping from max → `0` restores the whole-structure view. > Mirrors the "Next layer" button.
### `construct:prevlayer <instanceName>` ### `construct:prevlayer <instanceName>`
Step the layer down by one (wraps from `0` back to max). Step the layer down by one (wraps from `0` back to max).
@@ -112,7 +110,7 @@ Enable or disable a per-player builder option. Must be run as a player source.
## Information ## Information
### `construct:info <instanceName>` ### `construct:instanceinfo <instanceName>`
Print instance details to chat. Print instance details to chat.
> Outputs: bound structure ID, enabled state, placed location and dimension, current layer, verifier enabled, structure bounds (min/max). Equivalent to the data shown in the instance menu body. > Outputs: bound structure ID, enabled state, placed location and dimension, current layer, verifier enabled, structure bounds (min/max). Equivalent to the data shown in the instance menu body.
@@ -134,16 +132,12 @@ Print the material list for an instance.
## Utility ## Utility
### `construct:item` ### `construct:construct`
Give yourself the Construct menu item. Give yourself the Construct menu item.
> Already implemented as a native custom command. Needs to be refactored to fit the new command pipeline.
### `construct:tag <instanceName>` ### `construct:tag <instanceName>`
Rename the held Construct item to an instance name for quick-open. Errors if the item is not a construct item or if the instance name is not registered. Rename the held Construct item to an instance name for quick-open. Errors if the item is not a construct item or if the instance name is not registered.
| Argument | Description | | Argument | Description |
|---|---| |---|---|
| `<instanceName>` | Instance name to embed in the item's `nameTag`. Using the item in-world will jump straight to that instance's menu. | | `<instanceName>` | Instance name to embed in the item's `nameTag`. Using the item in-world will jump straight to that instance's menu. |
> The item-use handler in `construct.js` already checks `itemStack.nameTag` against known instance names; this command just makes it easy to tag an item without renaming it in an anvil.
+52
View File
@@ -0,0 +1,52 @@
"""Regolith filter: regenerate packs/BP/scripts/blocks.js from Mojang's
bedrock-samples blocks.json + sounds.json, in Construct's exact format.
Run it directly to replace blocks.js in place (no regolith export):
python filters/fetch_block_data/main.py
The JS-module renderer it shares with tools/bake_block_models lives in
tools/lib, hence the sys.path line below.
"""
import os
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tools" / "lib"))
from js_data import fetch, parse, render # noqa: E402
BLOCKS_URL = "https://raw.githubusercontent.com/Mojang/bedrock-samples/main/resource_pack/blocks.json"
SOUNDS_URL = "https://raw.githubusercontent.com/Mojang/bedrock-samples/main/resource_pack/sounds.json"
def build():
"""Fetch upstream data and return the full blocks.js file contents."""
blocks = parse(fetch(BLOCKS_URL))
blocks.pop("format_version", None)
sounds = parse(fetch(SOUNDS_URL))
block_sounds = sounds["block_sounds"]
return (
"export const blocks = " + render(blocks) + ";\n\n"
"export const block_sounds = " + render(block_sounds) + ";"
)
def output_path():
"""packs/BP/scripts/blocks.js in the project source tree."""
root = os.environ.get("ROOT_DIR")
root = Path(root) if root else Path(__file__).resolve().parents[2]
return root / "packs" / "BP" / "scripts" / "blocks.js"
def main():
contents = build()
dest = output_path()
dest.write_text(contents, encoding="utf-8") # no trailing newline, by design
print(f"[fetch_block_data] wrote {dest} ({len(contents)} bytes)")
if __name__ == "__main__":
main()
+28 -17
View File
@@ -5,8 +5,9 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"@minecraft/server": "^2.9.0-beta.1.26.30-stable", "@minecraft/debug-utilities": "1.0.0-beta.1.26.40-stable",
"@minecraft/server-ui": "^2.2.0-beta.1.26.30-stable" "@minecraft/server": "2.10.0-beta.1.26.40-stable",
"@minecraft/server-ui": "2.2.0-beta.1.26.40-stable"
} }
}, },
"node_modules/@minecraft/common": { "node_modules/@minecraft/common": {
@@ -16,24 +17,34 @@
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
"node_modules/@minecraft/server": { "node_modules/@minecraft/debug-utilities": {
"version": "2.9.0-rc.1.26.40-preview.24", "version": "1.0.0-beta.1.26.40-stable",
"resolved": "https://registry.npmjs.org/@minecraft/server/-/server-2.9.0-rc.1.26.40-preview.24.tgz", "resolved": "https://registry.npmjs.org/@minecraft/debug-utilities/-/debug-utilities-1.0.0-beta.1.26.40-stable.tgz",
"integrity": "sha512-/DH82sRkYjADbVh5lEzpQQYKy+oRuQqLLOMGI1Yml1Q/fd3nIHedveoGpU1GkL0hzNFTJGG/pi7D2pPJEhfGzw==", "integrity": "sha512-4JvxODw+8w0U3srQ23vZzmzt8HTginiOApszde9baW4NvS/nEz4KAlwlLRXqNcLTCCS2JNcg6vbgpjjlkHz4gg==",
"license": "MIT",
"peerDependencies": {
"@minecraft/common": "^1.2.0",
"@minecraft/vanilla-data": ">=1.20.70 || 1.26.40-preview.24"
}
},
"node_modules/@minecraft/server-ui": {
"version": "2.2.0-beta.1.26.30-stable",
"resolved": "https://registry.npmjs.org/@minecraft/server-ui/-/server-ui-2.2.0-beta.1.26.30-stable.tgz",
"integrity": "sha512-OMkGdrU5w/g/oIHR6ltpxbTNzEDYcDoHI56jW5FOBK+U6UwBO5wQteAPVvKKcBqD8KsY0R72xw1cKNfVwwJFIg==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@minecraft/common": "^1.0.0", "@minecraft/common": "^1.0.0",
"@minecraft/server": "^2.0.0 || ^2.9.0-beta.1.26.30-stable" "@minecraft/server": "^1.17.0 || ^2.0.0 || ^2.10.0-beta.1.26.40-stable"
}
},
"node_modules/@minecraft/server": {
"version": "2.10.0-beta.1.26.40-stable",
"resolved": "https://registry.npmjs.org/@minecraft/server/-/server-2.10.0-beta.1.26.40-stable.tgz",
"integrity": "sha512-PygRGiom/LRXV/fxcZ0ygCzCzF8pRp+PoqnVX+saazdOMXUvu940fZZjXCaTdjvasLp8tN4i3EgJV1aTxidKAQ==",
"license": "MIT",
"peerDependencies": {
"@minecraft/common": "^1.2.0",
"@minecraft/vanilla-data": ">=1.20.70"
}
},
"node_modules/@minecraft/server-ui": {
"version": "2.2.0-beta.1.26.40-stable",
"resolved": "https://registry.npmjs.org/@minecraft/server-ui/-/server-ui-2.2.0-beta.1.26.40-stable.tgz",
"integrity": "sha512-Ab/k5lYMktAjVttv3u//SCCc/AUmKnzJYA+ZvvAoKxQ1hEDwLC2OhGF+YF/S/mkSX6oPK8hD6I8bJBEq2CyCOw==",
"license": "MIT",
"peerDependencies": {
"@minecraft/common": "^1.0.0",
"@minecraft/server": "^2.0.0 || ^2.10.0-beta.1.26.40-stable"
} }
}, },
"node_modules/@minecraft/vanilla-data": { "node_modules/@minecraft/vanilla-data": {
+7 -2
View File
@@ -1,6 +1,11 @@
{ {
"type": "module",
"scripts": {
"test": "node --test \"test/**/*.test.js\""
},
"dependencies": { "dependencies": {
"@minecraft/server": "^2.9.0-beta.1.26.30-stable", "@minecraft/server": "2.10.0-beta.1.26.40-stable",
"@minecraft/server-ui": "^2.2.0-beta.1.26.30-stable" "@minecraft/server-ui": "2.2.0-beta.1.26.40-stable",
"@minecraft/debug-utilities": "1.0.0-beta.1.26.40-stable"
} }
} }
+9 -5
View File
@@ -1,11 +1,11 @@
{ {
"format_version": 2, "format_version": 2,
"header": { "header": {
"name": "Construct [BP] v1.1.0", "name": "Construct [BP] v1.2.0",
"description": "Survival building addon by §aForestOfLight§r.", "description": "Survival building addon by §aForestOfLight§r.",
"uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58", "uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58",
"min_engine_version": [1, 26, 30], "min_engine_version": [1, 26, 40],
"version": [1, 1, 0] "version": [1, 2, 0]
}, },
"modules": [ "modules": [
{ {
@@ -26,15 +26,19 @@
"dependencies": [ "dependencies": [
{ {
"module_name": "@minecraft/server", "module_name": "@minecraft/server",
"version": "2.9.0-beta" "version": "2.10.0-beta"
}, },
{ {
"module_name": "@minecraft/server-ui", "module_name": "@minecraft/server-ui",
"version": "2.2.0-beta" "version": "2.2.0-beta"
}, },
{
"module_name": "@minecraft/debug-utilities",
"version": "1.0.0-beta"
},
{ {
"uuid": "375ec465-3dc1-429f-8b4c-a337889e1ed4", "uuid": "375ec465-3dc1-429f-8b4c-a337889e1ed4",
"version": [1, 1, 0] "version": [1, 2, 0]
} }
], ],
"metadata": { "metadata": {
+3 -3
View File
@@ -1,18 +1,18 @@
import { AddonAPIServer } from "../lib/AddonAPIKit"; import { AddonAPIServer } from "../lib/AddonAPIKit";
import { PACK_IDENTIFIER } from "../consts"; import { PACK_IDENTIFIER } from "../consts";
import { InstancesController } from "./controllers/InstancesController"; import { InstancesController } from "./controllers/InstancesController";
import { structureCollection } from "../classes/Structure/StructureCollection"; import { instanceCollection } from "../classes/Instance/InstanceCollection";
import { BuildersController } from "./controllers/BuildersController"; import { BuildersController } from "./controllers/BuildersController";
import { Builders } from "../classes/Builder/Builders"; import { Builders } from "../classes/Builder/Builders";
class ConstructAPI extends AddonAPIServer { class ConstructAPI extends AddonAPIServer {
constructor(version) { constructor(version) {
super(PACK_IDENTIFIER, version); super(PACK_IDENTIFIER, version);
const instancesController = new InstancesController(structureCollection); const instancesController = new InstancesController(instanceCollection);
this.setupController(instancesController); this.setupController(instancesController);
const buildersController = new BuildersController(Builders); const buildersController = new BuildersController(Builders);
this.setupController(buildersController); this.setupController(buildersController);
} }
} }
export const constructAPI = new ConstructAPI("1.0.0"); export const constructAPI = new ConstructAPI("2.0.0");
+4 -2
View File
@@ -24,7 +24,8 @@ export const InstanceModel = PROTO.Object({
isEnabled: PROTO.Boolean, isEnabled: PROTO.Boolean,
trackPlayerDistance: PROTO.Int8, trackPlayerDistance: PROTO.Int8,
particleLifetime: PROTO.Int32 particleLifetime: PROTO.Int32
}) }),
renderMode: PROTO.String
}); });
export const InstancesModel = PROTO.Array(InstanceModel); export const InstancesModel = PROTO.Array(InstanceModel);
@@ -52,7 +53,8 @@ export const BuilderModel = PROTO.Object({
easyPlace: PROTO.Boolean, easyPlace: PROTO.Boolean,
fastEasyPlace: PROTO.Boolean, fastEasyPlace: PROTO.Boolean,
materialGrabber: PROTO.Boolean, materialGrabber: PROTO.Boolean,
materialInstanceName: PROTO.String materialInstanceName: PROTO.String,
defaultRenderMode: PROTO.String
}); });
export const BuilderIdParameterModel = PROTO.Object({ export const BuilderIdParameterModel = PROTO.Object({
+9
View File
@@ -0,0 +1,9 @@
export const atlasWidth = 1024;
export const atlasHeight = 1024;
export const whiteUvRect = {
"h" : 15.0,
"w" : 15.0,
"x" : 320.5,
"y" : 384.5
};
File diff suppressed because it is too large Load Diff
+264 -58
View File
@@ -1080,6 +1080,10 @@ export const blocks = {
"west" : "chiseled_bookshelf_side" "west" : "chiseled_bookshelf_side"
} }
}, },
"chiseled_cinnabar" : {
"sound" : "cinnabar",
"textures" : "chiseled_cinnabar"
},
"chiseled_copper" : { "chiseled_copper" : {
"sound" : "copper", "sound" : "copper",
"textures" : "chiseled_copper" "textures" : "chiseled_copper"
@@ -1139,6 +1143,10 @@ export const blocks = {
"sound" : "stone", "sound" : "stone",
"textures" : "chiseled_stone_bricks" "textures" : "chiseled_stone_bricks"
}, },
"chiseled_sulfur" : {
"sound" : "sulfur",
"textures" : "chiseled_sulfur"
},
"chiseled_tuff" : { "chiseled_tuff" : {
"sound" : "tuff", "sound" : "tuff",
"textures" : { "textures" : {
@@ -1163,6 +1171,46 @@ export const blocks = {
"sound" : "stone", "sound" : "stone",
"textures" : "chorus_plant" "textures" : "chorus_plant"
}, },
"cinnabar" : {
"sound" : "cinnabar",
"textures" : "cinnabar"
},
"cinnabar_brick_double_slab" : {
"sound" : "cinnabar",
"textures" : "cinnabar_bricks"
},
"cinnabar_brick_slab" : {
"sound" : "cinnabar",
"textures" : "cinnabar_bricks"
},
"cinnabar_brick_stairs" : {
"sound" : "cinnabar",
"textures" : "cinnabar_bricks"
},
"cinnabar_brick_wall" : {
"sound" : "cinnabar",
"textures" : "cinnabar_bricks"
},
"cinnabar_bricks" : {
"sound" : "cinnabar",
"textures" : "cinnabar_bricks"
},
"cinnabar_double_slab" : {
"sound" : "cinnabar",
"textures" : "cinnabar"
},
"cinnabar_slab" : {
"sound" : "cinnabar",
"textures" : "cinnabar"
},
"cinnabar_stairs" : {
"sound" : "cinnabar",
"textures" : "cinnabar"
},
"cinnabar_wall" : {
"sound" : "cinnabar",
"textures" : "cinnabar"
},
"clay" : { "clay" : {
"isotropic" : true, "isotropic" : true,
"sound" : "gravel", "sound" : "gravel",
@@ -2500,7 +2548,6 @@ export const blocks = {
"up" : "still_water_grey" "up" : "still_water_grey"
} }
}, },
"format_version" : "1.21.40",
"frame" : { "frame" : {
"sound" : "itemframe", "sound" : "itemframe",
"textures" : "itemframe_background" "textures" : "itemframe_background"
@@ -2569,6 +2616,10 @@ export const blocks = {
"sound" : "stone", "sound" : "stone",
"textures" : "gold_ore" "textures" : "gold_ore"
}, },
"golden_dandelion" : {
"sound" : "grass",
"textures" : "golden_dandelion"
},
"golden_rail" : { "golden_rail" : {
"sound" : "metal", "sound" : "metal",
"textures" : { "textures" : {
@@ -4426,6 +4477,26 @@ export const blocks = {
"sound" : "stone", "sound" : "stone",
"textures" : "polished_blackstone" "textures" : "polished_blackstone"
}, },
"polished_cinnabar" : {
"sound" : "cinnabar",
"textures" : "polished_cinnabar"
},
"polished_cinnabar_double_slab" : {
"sound" : "cinnabar",
"textures" : "polished_cinnabar"
},
"polished_cinnabar_slab" : {
"sound" : "cinnabar",
"textures" : "polished_cinnabar"
},
"polished_cinnabar_stairs" : {
"sound" : "cinnabar",
"textures" : "polished_cinnabar"
},
"polished_cinnabar_wall" : {
"sound" : "cinnabar",
"textures" : "polished_cinnabar"
},
"polished_deepslate" : { "polished_deepslate" : {
"isotropic" : false, "isotropic" : false,
"sound" : "deepslate", "sound" : "deepslate",
@@ -4483,6 +4554,26 @@ export const blocks = {
"sound" : "stone", "sound" : "stone",
"textures" : "polished_granite" "textures" : "polished_granite"
}, },
"polished_sulfur" : {
"sound" : "sulfur",
"textures" : "polished_sulfur"
},
"polished_sulfur_double_slab" : {
"sound" : "sulfur",
"textures" : "polished_sulfur"
},
"polished_sulfur_slab" : {
"sound" : "sulfur",
"textures" : "polished_sulfur"
},
"polished_sulfur_stairs" : {
"sound" : "sulfur",
"textures" : "polished_sulfur"
},
"polished_sulfur_wall" : {
"sound" : "sulfur",
"textures" : "polished_sulfur"
},
"polished_tuff" : { "polished_tuff" : {
"sound" : "polished_tuff", "sound" : "polished_tuff",
"textures" : "polished_tuff" "textures" : "polished_tuff"
@@ -4515,6 +4606,10 @@ export const blocks = {
"sound" : "grass", "sound" : "grass",
"textures" : "potatoes" "textures" : "potatoes"
}, },
"potent_sulfur" : {
"sound" : "potent_sulfur",
"textures" : "potent_sulfur"
},
"powder_snow" : { "powder_snow" : {
"isotropic" : false, "isotropic" : false,
"sound" : "powder_snow", "sound" : "powder_snow",
@@ -5743,6 +5838,57 @@ export const blocks = {
"structure_void" : { "structure_void" : {
"textures" : "flattened_structure_void" "textures" : "flattened_structure_void"
}, },
"sulfur" : {
"sound" : "sulfur",
"textures" : "sulfur"
},
"sulfur_brick_double_slab" : {
"sound" : "sulfur",
"textures" : "sulfur_bricks"
},
"sulfur_brick_slab" : {
"sound" : "sulfur",
"textures" : "sulfur_bricks"
},
"sulfur_brick_stairs" : {
"sound" : "sulfur",
"textures" : "sulfur_bricks"
},
"sulfur_brick_wall" : {
"sound" : "sulfur",
"textures" : "sulfur_bricks"
},
"sulfur_bricks" : {
"sound" : "sulfur",
"textures" : "sulfur_bricks"
},
"sulfur_double_slab" : {
"sound" : "sulfur",
"textures" : "sulfur"
},
"sulfur_slab" : {
"sound" : "sulfur",
"textures" : "sulfur"
},
"sulfur_spike" : {
"sound" : "sulfur",
"textures" : {
"down" : "sulfur_spike_frustum",
"east" : "sulfur_spike_base",
"north" : "sulfur_spike_tip",
"south" : "sulfur_spike_middle",
"up" : "sulfur_spike_base",
"west" : "sulfur_spike_merge"
}
},
"sulfur_stairs" : {
"sound" : "sulfur",
"textures" : "sulfur"
},
"sulfur_wall" : {
"sound" : "sulfur",
"textures" : "sulfur"
},
"sunflower" : { "sunflower" : {
"carried_textures" : "sunflower_carried", "carried_textures" : "sunflower_carried",
"sound" : "grass", "sound" : "grass",
@@ -6850,7 +6996,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.ancient_debris", "sound" : "place.ancient_debris",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -6953,7 +7099,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.azalea_leaves", "sound" : "place.azalea_leaves",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -7127,7 +7273,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "break.bamboo_wood_hanging_sign", "sound" : "place.bamboo_wood_hanging_sign",
"volume" : 1.0 "volume" : 1.0
} }
} }
@@ -7151,7 +7297,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.basalt", "sound" : "place.basalt",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -7219,7 +7365,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.bone_block", "sound" : "place.bone_block",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -7299,7 +7445,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : 1.0, "pitch" : 1.0,
"sound" : "dig.candle", "sound" : "place.candle",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -7333,7 +7479,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.cave_vines", "sound" : "place.cave_vines",
"volume" : 1.0 "volume" : 1.0
} }
}, },
@@ -7359,7 +7505,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.chain", "sound" : "place.chain",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -7488,7 +7634,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "break.cherry_wood_hanging_sign", "sound" : "place.cherry_wood_hanging_sign",
"volume" : 1.0 "volume" : 1.0
} }
} }
@@ -7535,6 +7681,25 @@ export const block_sounds = {
"pitch" : 1.0, "pitch" : 1.0,
"volume" : 1.0 "volume" : 1.0
}, },
"cinnabar" : {
"events" : {
"break" : {
"pitch" : 0.80,
"sound" : "block.cinnabar.break"
},
"hit" : {
"pitch" : 0.50,
"sound" : "block.cinnabar.hit",
"volume" : 0.30
},
"place" : {
"pitch" : 0.80,
"sound" : "block.cinnabar.place"
}
},
"pitch" : 1.0,
"volume" : 1.0
},
"cloth" : { "cloth" : {
"events" : { "events" : {
"break" : { "break" : {
@@ -7554,7 +7719,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.cloth", "sound" : "place.cloth",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -7588,7 +7753,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 0.80 ], "pitch" : [ 0.80, 0.80 ],
"sound" : "dig.wood", "sound" : "place.wood",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -7630,7 +7795,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.copper", "sound" : "place.copper",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -7750,7 +7915,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : 0.80, "pitch" : 0.80,
"sound" : "break.copper_grate", "sound" : "place.copper_grate",
"volume" : 1.0 "volume" : 1.0
} }
}, },
@@ -7776,7 +7941,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.coral", "sound" : "place.coral",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -7843,7 +8008,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : 1.0, "pitch" : 1.0,
"sound" : "break.decorated_pot", "sound" : "place.decorated_pot",
"volume" : 1.0 "volume" : 1.0
}, },
"shatter_pot" : { "shatter_pot" : {
@@ -8059,7 +8224,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : 1.50, "pitch" : 1.50,
"sound" : "break.frog_spawn", "sound" : "place.frog_spawn",
"volume" : 0.20 "volume" : 0.20
} }
} }
@@ -8082,7 +8247,7 @@ export const block_sounds = {
"volume" : 1.0 "volume" : 1.0
}, },
"place" : { "place" : {
"sound" : "break.froglight", "sound" : "place.froglight",
"volume" : 1.0 "volume" : 1.0
} }
}, },
@@ -8107,7 +8272,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.fungus", "sound" : "place.fungus",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -8141,7 +8306,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.stone", "sound" : "place.stone",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -8175,7 +8340,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.grass", "sound" : "place.grass",
"volume" : 0.80 "volume" : 0.80
} }
}, },
@@ -8201,7 +8366,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.grass", "sound" : "place.grass",
"volume" : 0.80 "volume" : 0.80
}, },
"power.off" : { "power.off" : {
@@ -8235,7 +8400,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.gravel", "sound" : "place.gravel",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -8303,7 +8468,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "break.hanging_sign", "sound" : "place.hanging_sign",
"volume" : 1.0 "volume" : 1.0
} }
} }
@@ -8327,7 +8492,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : 0.80, "pitch" : 0.80,
"sound" : "break.heavy_core", "sound" : "place.heavy_core",
"volume" : 1.0 "volume" : 1.0
} }
}, },
@@ -8353,7 +8518,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.honey_block", "sound" : "place.honey_block",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -8479,7 +8644,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.wood", "sound" : "place.wood",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -8606,7 +8771,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 0.80 ], "pitch" : [ 0.80, 0.80 ],
"sound" : "dig.wood", "sound" : "place.wood",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -8640,7 +8805,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.lodestone", "sound" : "place.lodestone",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -8745,7 +8910,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 1.20, 1.250 ], "pitch" : [ 1.20, 1.250 ],
"sound" : "dig.stone", "sound" : "place.stone",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -8785,7 +8950,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : 0.80, "pitch" : 0.80,
"sound" : "block.mob_spawner.break" "sound" : "block.mob_spawner.place"
} }
}, },
"pitch" : 1.0, "pitch" : 1.0,
@@ -8925,7 +9090,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.nether_brick", "sound" : "place.nether_brick",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -8959,7 +9124,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.nether_gold_ore", "sound" : "place.nether_gold_ore",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -8993,7 +9158,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 1.20, 1.250 ], "pitch" : [ 1.20, 1.250 ],
"sound" : "dig.nether_sprouts", "sound" : "place.nether_sprouts",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -9027,7 +9192,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.nether_wart", "sound" : "place.nether_wart",
"volume" : 0.70 "volume" : 0.70
}, },
"power.off" : { "power.off" : {
@@ -9133,7 +9298,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "break.nether_wood_hanging_sign", "sound" : "place.nether_wood_hanging_sign",
"volume" : 1.0 "volume" : 1.0
} }
} }
@@ -9157,7 +9322,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.netherite", "sound" : "place.netherite",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -9191,7 +9356,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.netherrack", "sound" : "place.netherrack",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -9230,7 +9395,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.stone", "sound" : "place.stone",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -9264,7 +9429,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 0.80 ], "pitch" : [ 0.80, 0.80 ],
"sound" : "dig.nylium", "sound" : "place.nylium",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -9386,6 +9551,28 @@ export const block_sounds = {
"pitch" : 1.20, "pitch" : 1.20,
"volume" : 1.0 "volume" : 1.0
}, },
"potent_sulfur" : {
"events" : {
"ambient" : {
"sound" : "block.potent_sulfur.ambient"
},
"break" : {
"pitch" : 0.80,
"sound" : "block.potent_sulfur.break"
},
"hit" : {
"pitch" : 0.50,
"sound" : "block.potent_sulfur.hit",
"volume" : 0.30
},
"place" : {
"pitch" : 0.80,
"sound" : "block.potent_sulfur.place"
}
},
"pitch" : 1.0,
"volume" : 1.0
},
"powder_snow" : { "powder_snow" : {
"events" : { "events" : {
"break" : { "break" : {
@@ -9405,7 +9592,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.powder_snow", "sound" : "place.powder_snow",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -9483,7 +9670,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.roots", "sound" : "place.roots",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -9518,7 +9705,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.sand", "sound" : "place.sand",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -9587,7 +9774,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "break.sculk", "sound" : "place.sculk",
"volume" : 1.0 "volume" : 1.0
} }
}, },
@@ -9614,7 +9801,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "break.sculk_catalyst", "sound" : "place.sculk_catalyst",
"volume" : 1.0 "volume" : 1.0
} }
} }
@@ -9673,7 +9860,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "break.sculk_shrieker", "sound" : "place.sculk_shrieker",
"volume" : 1.0 "volume" : 1.0
} }
} }
@@ -9698,7 +9885,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "break.sculk_vein", "sound" : "place.sculk_vein",
"volume" : 1.0 "volume" : 1.0
} }
} }
@@ -9735,7 +9922,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.shroomlight", "sound" : "place.shroomlight",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -9769,7 +9956,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "mob.slime.big", "sound" : "place.slime.big",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -9834,7 +10021,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.snow", "sound" : "place.snow",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -9868,7 +10055,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.soul_sand", "sound" : "place.soul_sand",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -9902,7 +10089,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.soul_soil", "sound" : "place.soul_soil",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -9996,7 +10183,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.stem", "sound" : "place.stem",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -10038,7 +10225,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.stone", "sound" : "place.stone",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -10059,6 +10246,25 @@ export const block_sounds = {
"pitch" : 1.0, "pitch" : 1.0,
"volume" : 1.0 "volume" : 1.0
}, },
"sulfur" : {
"events" : {
"break" : {
"pitch" : 0.80,
"sound" : "block.sulfur.break"
},
"hit" : {
"pitch" : 0.50,
"sound" : "block.sulfur.hit",
"volume" : 0.30
},
"place" : {
"pitch" : 0.80,
"sound" : "block.sulfur.place"
}
},
"pitch" : 1.0,
"volume" : 1.0
},
"suspicious_gravel" : { "suspicious_gravel" : {
"events" : { "events" : {
"break" : { "break" : {
@@ -10275,7 +10481,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.stone", "sound" : "place.stone",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -10331,7 +10537,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 1.0 ], "pitch" : [ 0.80, 1.0 ],
"sound" : "dig.vines", "sound" : "place.vines",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -10361,7 +10567,7 @@ export const block_sounds = {
"sound" : "step.web" "sound" : "step.web"
}, },
"place" : { "place" : {
"sound" : "break.web" "sound" : "place.web"
} }
}, },
"pitch" : 1.0, "pitch" : 1.0,
@@ -10462,7 +10668,7 @@ export const block_sounds = {
}, },
"place" : { "place" : {
"pitch" : [ 0.80, 0.80 ], "pitch" : [ 0.80, 0.80 ],
"sound" : "dig.wood", "sound" : "place.wood",
"volume" : 1.0 "volume" : 1.0
}, },
"power.off" : { "power.off" : {
@@ -0,0 +1,24 @@
import { world } from "@minecraft/server";
import { blockPlacementSignal } from "./BlockPlacementSignal";
import { ChangedCellPatcher } from "./Verifier/ChangedCellPatcher";
export class BlockChangeWatcher {
#patcher = new ChangedCellPatcher();
start() {
world.afterEvents.playerPlaceBlock.subscribe((event) => this.#onBlockChanged(event.block));
world.afterEvents.playerBreakBlock.subscribe((event) => this.#onBlockChanged(event.block));
world.afterEvents.playerInteractWithBlock.subscribe((event) => {
if (event.isFirstEvent)
this.#onBlockChanged(event.block);
});
blockPlacementSignal.subscribe((dimensionId, location) => this.#patcher.patchAround(dimensionId, location));
}
#onBlockChanged(block) {
this.#patcher.patchAround(block.dimension.id, block.location);
}
}
export const blockChangeWatcher = new BlockChangeWatcher();
blockChangeWatcher.start();
+39 -32
View File
@@ -1,7 +1,9 @@
import { GameMode, system, world } from '@minecraft/server'; import { GameMode, system, world } from '@minecraft/server';
import { Raycaster } from '../classes/Raycaster'; import { Raycaster } from '../classes/Raycaster';
import { fetchMatchingItemSlot } from '../utils'; import { fetchMatchingItemSlot } from '../utils';
import { Block_state, chinese_text, State_values } from './zhMappings'; import { Builders } from './Builder/Builders';
import { isBannedBlock } from '../options/fastEasyPlace';
class BlockInfo { class BlockInfo {
static shownToLastTick = new Set(); static shownToLastTick = new Set();
@@ -14,41 +16,46 @@ class BlockInfo {
} }
static showStructureBlockInfo(player) { static showStructureBlockInfo(player) {
const block = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: true, useActiveLayer: true }); const hit = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: true, useActiveLayer: true });
if (!block && this.shownToLastTick.has(player.id)) { if (!hit && this.shownToLastTick.has(player.id)) {
player.onScreenDisplay.setActionBar({ rawtext: [ player.onScreenDisplay.setActionBar({ rawtext: [
{ translate: 'construct.blockinfo.header' }, this.getFormattedHeader(),
{ text: '\n' },
{ translate: 'construct.blockinfo.none' } { translate: 'construct.blockinfo.none' }
]}); ]});
this.shownToLastTick.delete(player.id); this.shownToLastTick.delete(player.id);
} }
if (!block) if (!hit)
return; return;
player.onScreenDisplay.setActionBar(this.getFormattedBlockInfo(player, block.permutation)); player.onScreenDisplay.setActionBar(this.getFormattedBlockInfo(player, hit));
this.shownToLastTick.add(player.id); this.shownToLastTick.add(player.id);
} }
static getFormattedBlockInfo(player, block) { static getFormattedBlockInfo(player, hit) {
const easyPlaceMessage = this.getEasyPlaceMessage(player, hit);
const supplyMessage = this.getSupplyMessage(player, hit);
return { rawtext: [
this.getFormattedHeader(hit.instance),
this.getBlockMessage(hit.block),
{ text: '\n' },
easyPlaceMessage,
(easyPlaceMessage.translate && supplyMessage.translate) ? { text: ' ' } : { text: '' },
supplyMessage
] };
}
static getFormattedHeader(instance = void 0) {
return { rawtext: [ return { rawtext: [
{ translate: 'construct.blockinfo.header' }, { translate: 'construct.blockinfo.header' },
this.getSupplyMessage(player, block), (instance != void 0) ? { text: ` §7${instance.getName()}\n` } : { text: '\n' }
{ text: '\n' }, ]};
this.getBlockMessage(block)
] };
} }
static getBlockMessage(block) { static getBlockMessage(block) {
if (!block) if (!block)
return { translate: 'construct.blockinfo.unknown' }; return { translate: 'construct.blockinfo.unknown' };
const message = { rawtext: [{ text: '§a' }]}; const message = { rawtext: [{ text: '§a' }, { translate: block.permutation.localizationKey }] };
if (chinese_text[block.type.id] !== undefined) { const states = block.states;
message.rawtext.push({ text: chinese_text[block.type.id] }); if (block.hasStates)
} else {
message.rawtext.push({ translate: block.localizationKey });
}
const states = block.getAllStates();
if (Object.keys(states).length > 0)
message.rawtext.push({ text: `\n§7${this.getFormattedStates(states)}` }); message.rawtext.push({ text: `\n§7${this.getFormattedStates(states)}` });
if (block.isWaterlogged) { if (block.isWaterlogged) {
message.rawtext.push({ rawtext: [ message.rawtext.push({ rawtext: [
@@ -60,21 +67,21 @@ class BlockInfo {
} }
static getFormattedStates(states) { static getFormattedStates(states) {
return Object.entries(states) return Object.entries(states).map(([key, value]) => `§7${key}: §3${value}`).join('\n');
.map(([stateName, stateValue]) => {
if (State_values[stateName] || Block_state[stateName]) {
return (`\n§7${Block_state[stateName]}`)
.replaceAll('%1', `§3${State_values[stateName][String(stateValue)]}§7`);
}
})
.join('');
} }
static getSupplyMessage(player, block) { static getSupplyMessage(player, hit) {
const itemStack = fetchMatchingItemSlot(player, block.getItemStack()?.typeId); if (player.getGameMode() !== GameMode.Survival || fetchMatchingItemSlot(player, hit.block?.permutation.getItemStack()?.typeId))
const isInSurvival = player.getGameMode() === GameMode.Survival; return { text: '' };
if (!itemStack && isInSurvival)
return { translate: 'construct.blockinfo.nosupply' }; return { translate: 'construct.blockinfo.nosupply' };
}
static getEasyPlaceMessage(player, hit) {
const builder = Builders.get(player.id);
if (!builder.isOptionEnabled('easyPlace') && !builder.isOptionEnabled('fastEasyPlace'))
return { text: '' };
if (isBannedBlock(player, hit.block))
return { translate: 'construct.blockinfo.noeasyplace' };
return { text: '' }; return { text: '' };
} }
} }
@@ -0,0 +1,14 @@
class BlockPlacementSignal {
#callbacks = [];
subscribe(callback) {
this.#callbacks.push(callback);
}
emit(dimensionId, location) {
for (const callback of this.#callbacks)
callback(dimensionId, location);
}
}
export const blockPlacementSignal = new BlockPlacementSignal();
+7 -1
View File
@@ -13,6 +13,10 @@ export class Builder {
return BuilderOptions.isEnabled(optionId, this.playerId); return BuilderOptions.isEnabled(optionId, this.playerId);
} }
getOptionValue(optionId) {
return BuilderOptions.getValue(optionId, this.playerId);
}
setOption(optionId, value) { setOption(optionId, value) {
return BuilderOptions.setValue(optionId, this.playerId, value); return BuilderOptions.setValue(optionId, this.playerId, value);
} }
@@ -27,7 +31,8 @@ export class Builder {
easyPlace: this.isOptionEnabled('easyPlace'), easyPlace: this.isOptionEnabled('easyPlace'),
fastEasyPlace: this.isOptionEnabled('fastEasyPlace'), fastEasyPlace: this.isOptionEnabled('fastEasyPlace'),
materialGrabber: this.isOptionEnabled('materialGrabber'), materialGrabber: this.isOptionEnabled('materialGrabber'),
materialInstanceName: this.materialInstanceName materialInstanceName: this.materialInstanceName,
defaultRenderMode: this.getOptionValue('defaultRenderMode')
}; };
} }
@@ -35,6 +40,7 @@ export class Builder {
this.setOption('easyPlace', builderOptions.easyPlace); this.setOption('easyPlace', builderOptions.easyPlace);
this.setOption('fastEasyPlace', builderOptions.fastEasyPlace); this.setOption('fastEasyPlace', builderOptions.fastEasyPlace);
this.setOption('materialGrabber', builderOptions.materialGrabber); this.setOption('materialGrabber', builderOptions.materialGrabber);
this.setOption('defaultRenderMode', builderOptions.defaultRenderMode);
this.materialInstanceName = builderOptions.materialInstanceName; this.materialInstanceName = builderOptions.materialInstanceName;
} }
} }
@@ -0,0 +1,60 @@
import { BuilderOption } from "./BuilderOption";
import { world } from "@minecraft/server";
export class BuilderChoiceOption extends BuilderOption {
choices;
choiceLabels;
defaultValue;
constructor({ identifier, displayName, description, choices, choiceLabels, defaultValue, onChangeCallback = () => {} }) {
super({ identifier, displayName, description, howToUse: description });
this.choices = choices;
this.choiceLabels = choiceLabels;
this.defaultValue = defaultValue;
this.onChange = onChangeCallback;
}
getValue(playerId) {
const stored = world.getDynamicProperty(this.dynamicPropertyKey(playerId));
return this.choices.includes(stored) ? stored : this.defaultValue;
}
isEnabled(playerId) {
return this.getValue(playerId) !== this.defaultValue;
}
setValue(playerId, value) {
if (!this.choices.includes(value))
return void 0;
const previous = this.getValue(playerId);
this.save(playerId, value);
if (previous === value)
return void 0;
this.onChange(playerId, value);
return value;
}
getLabel(value) {
return this.choiceLabels[this.choices.indexOf(value)];
}
addControlTo(form, playerId) {
form.dropdown(this.displayName, this.choiceLabels, {
defaultValueIndex: Math.max(this.choices.indexOf(this.getValue(playerId)), 0),
tooltip: this.description
});
}
applyFormValue(playerId, formValue) {
const changedToValue = this.setValue(playerId, this.choices[formValue]);
if (changedToValue === void 0)
return void 0;
return { rawtext: [
{ text: `§a` },
this.displayName,
{ translate: 'construct.option.setto' },
this.getLabel(changedToValue),
{ text: `§a.` }
]};
}
}
@@ -18,31 +18,9 @@ export class BuilderForm {
applySettings(formValues) { applySettings(formValues) {
const optionIds = BuilderOptions.getOptionIds(); const optionIds = BuilderOptions.getOptionIds();
for (let i = 0; i < optionIds.length; i++) { for (let i = 0; i < optionIds.length; i++) {
const option = BuilderOptions.get(optionIds[i]); const message = BuilderOptions.get(optionIds[i]).applyFormValue(this.player.id, formValues[i]);
const changedToValue = option.setValue(this.player.id, formValues[i]); if (message)
if (changedToValue === true) { this.player.sendMessage(message);
this.player.sendMessage({ rawtext: [
{ text: `§a` },
option.displayName,
{ translate: 'construct.option.enabled' },
{ text: `§7 ` },
option.howToUse
]});
} else if (changedToValue === false) {
this.player.sendMessage({ rawtext: [
{ text: `§c` },
option.displayName,
{ translate: 'construct.option.disabled' }
]});
} }
} }
}
valueChangedToEnabled(hasChanged, newValue) {
return hasChanged && newValue === true;
}
valueChangedToDisabled(hasChanged, newValue) {
return hasChanged && newValue === false;
}
} }
@@ -6,10 +6,8 @@ export class BuilderFormBuilder {
static buildBuilderOptions(player) { static buildBuilderOptions(player) {
const form = new ModalFormData() const form = new ModalFormData()
.title(MenuFormBuilder.menuTitle); .title(MenuFormBuilder.menuTitle);
for (const optionId of BuilderOptions.getOptionIds()) { for (const optionId of BuilderOptions.getOptionIds())
const option = BuilderOptions.get(optionId); BuilderOptions.get(optionId).addControlTo(form, player.id);
form.toggle(option.displayName, { defaultValue: option.isEnabled(player.id), tooltip: option.description });
}
form.submitButton({ translate: "construct.menu.submit" }); form.submitButton({ translate: "construct.menu.submit" });
return form; return form;
} }
@@ -1,6 +1,8 @@
import { BuilderOptions } from "./BuilderOptions"; import { BuilderOptions } from "./BuilderOptions";
import { world } from "@minecraft/server"; import { world } from "@minecraft/server";
const DP_NAMESPACE = "builderOptions";
export class BuilderOption { export class BuilderOption {
identifier; identifier;
displayName; displayName;
@@ -8,7 +10,6 @@ export class BuilderOption {
howToUse; howToUse;
#onEnable; #onEnable;
#onDisable; #onDisable;
#DP_NAMESPACE = "builderOptions";
constructor({ identifier, displayName, description, howToUse, onEnableCallback = () => {}, onDisableCallback = () => {} }) { constructor({ identifier, displayName, description, howToUse, onEnableCallback = () => {}, onDisableCallback = () => {} }) {
this.identifier = identifier; this.identifier = identifier;
@@ -20,8 +21,16 @@ export class BuilderOption {
BuilderOptions.add(this); BuilderOptions.add(this);
} }
dynamicPropertyKey(playerId) {
return `${DP_NAMESPACE}:${playerId}:${this.identifier}`;
}
isEnabled(playerId) { isEnabled(playerId) {
return world.getDynamicProperty(`${this.#DP_NAMESPACE}:${playerId}:${this.identifier}`) === true; return world.getDynamicProperty(this.dynamicPropertyKey(playerId)) === true;
}
getValue(playerId) {
return this.isEnabled(playerId);
} }
setValue(playerId, value) { setValue(playerId, value) {
@@ -38,6 +47,29 @@ export class BuilderOption {
} }
save(playerId, value) { save(playerId, value) {
world.setDynamicProperty(`${this.#DP_NAMESPACE}:${playerId}:${this.identifier}`, value); world.setDynamicProperty(this.dynamicPropertyKey(playerId), value);
}
addControlTo(form, playerId) {
form.toggle(this.displayName, { defaultValue: this.isEnabled(playerId), tooltip: this.description });
}
applyFormValue(playerId, formValue) {
const changedToValue = this.setValue(playerId, formValue);
if (changedToValue === void 0)
return void 0;
if (changedToValue)
return { rawtext: [
{ text: `§a` },
this.displayName,
{ translate: 'construct.option.enabled' },
{ text: `§7 ` },
this.howToUse
]};
return { rawtext: [
{ text: `§c` },
this.displayName,
{ translate: 'construct.option.disabled' }
]};
} }
} }
@@ -17,6 +17,10 @@ export class BuilderOptions {
return this.options[optionId].isEnabled(playerId); return this.options[optionId].isEnabled(playerId);
} }
static getValue(optionId, playerId) {
return this.options[optionId].getValue(playerId);
}
static setValue(optionId, playerId, value) { static setValue(optionId, playerId, value) {
return this.options[optionId].setValue(playerId, value); return this.options[optionId].setValue(playerId, value);
} }
@@ -0,0 +1,31 @@
export const RenderMode = Object.freeze({
Default: "default",
Performance: "performance",
PreviewOnly: "previewOnly",
Classic: "classic"
});
export const RENDER_MODE_ORDER = Object.freeze([
RenderMode.Default,
RenderMode.Performance,
RenderMode.PreviewOnly,
RenderMode.Classic
]);
export const RENDER_MODE_LABELS = Object.freeze({
[RenderMode.Default]: "construct.instance.option.rendermode.default",
[RenderMode.Performance]: "construct.instance.option.rendermode.performance",
[RenderMode.PreviewOnly]: "construct.instance.option.rendermode.previewonly",
[RenderMode.Classic]: "construct.instance.option.rendermode.classic"
});
const PROFILES = Object.freeze({
[RenderMode.Default]: Object.freeze({ debugMarkers: true, particleOverlays: true, blockPreview: true, hybridOutline: true }),
[RenderMode.Performance]: Object.freeze({ debugMarkers: true, particleOverlays: false, blockPreview: false, hybridOutline: true }),
[RenderMode.PreviewOnly]: Object.freeze({ debugMarkers: false, particleOverlays: true, blockPreview: true, hybridOutline: true }),
[RenderMode.Classic]: Object.freeze({ debugMarkers: false, particleOverlays: true, blockPreview: false, hybridOutline: false })
});
export function renderProfileOf(mode) {
return PROFILES[mode] ?? PROFILES[RenderMode.Default];
}
+28
View File
@@ -0,0 +1,28 @@
import { Vector } from "../../lib/Vector.js";
// The six sides of a block, in the order the baked face table indexes them (see _CULL_DIRECTIONS in tools/bake_block_models/main.py).
// A baked face's cull value is a subscript into this, so reordering it alone would cull against the wrong side of the block.
export class Side {
static Down = 0;
static Up = 1;
static North = 2;
static South = 3;
static West = 4;
static East = 5;
static COUNT = 6;
static OFFSETS = Object.freeze([
Vector.down,
Vector.up,
Vector.north,
Vector.south,
Vector.west,
Vector.east
]);
// The order above is laid out in opposite pairs, so flipping the low bit turns a side into the one facing back at it.
static opposite(side) {
return side ^ 1;
}
}
@@ -1,16 +1,19 @@
import { InputPermissionCategory, world, system } from "@minecraft/server"; import { InputPermissionCategory, world, system } from "@minecraft/server";
import { Outliner } from "../Outliner"; import { createOutlineRenderer } from "../Render/outline/createOutlineRenderer";
import { MENU_ITEM } from "../../consts"; import { MENU_ITEM } from "../../consts";
import { Vector } from "../../lib/Vector"; import { Vector } from "../../lib/Vector";
import { PlayerMovement } from "../PlayerMovement"; import { PlayerMovement } from "../PlayerMovement";
import { Builders } from "../Builder/Builders"; import { Builders } from "../Builder/Builders";
const MOVE_OUTLINE_TIMING = Object.freeze({ drawIntervalTicks: 1, lifetimeTicks: 1 });
export class FlexibleInstanceMove { export class FlexibleInstanceMove {
player; player;
instance; instance;
outliner; outliner;
currentInstanceLocation; currentInstanceLocation;
runner = void 0; runner = void 0;
#velocity = new Vector();
constructor(instance, player) { constructor(instance, player) {
this.instance = instance; this.instance = instance;
@@ -38,10 +41,15 @@ export class FlexibleInstanceMove {
prepInstanceForMovement() { prepInstanceForMovement() {
this.instance.flexMovingPlayerId = this.player.id; this.instance.flexMovingPlayerId = this.player.id;
this.instance.disable(); this.instance.disable();
const bounds = this.instance.getBounds(); const { min, max } = this.outlineBounds();
const maxWorldLocation = this.currentInstanceLocation.add(Vector.from(bounds.max)); this.outliner = createOutlineRenderer(
this.outliner = new Outliner(this.instance.getDimension(), this.currentInstanceLocation, maxWorldLocation, 1, 1); this.instance.getRenderMode(),
this.outliner.startDraw(); this.instance.getDimension(),
min,
max,
MOVE_OUTLINE_TIMING
);
this.outliner.start();
} }
prepPlayerForMovement() { prepPlayerForMovement() {
@@ -63,17 +71,19 @@ export class FlexibleInstanceMove {
const speedFactor = 0.5; const speedFactor = 0.5;
const viewDir = playerMovement.getMajorDirectionFacing(); const viewDir = playerMovement.getMajorDirectionFacing();
const forward = new Vector(viewDir.x, viewDir.y, viewDir.z);
const right = new Vector(forward.z, 0, -forward.x);
const moveInput = playerMovement.getMovementVector(); const moveInput = playerMovement.getMovementVector();
let velocity = forward.multiply(moveInput.y).add(right.multiply(moveInput.x)); const velocity = this.#velocity.set(
viewDir.x * moveInput.y + viewDir.z * moveInput.x,
viewDir.y * moveInput.y,
viewDir.z * moveInput.y - viewDir.x * moveInput.x
);
if (playerMovement.isJumping()) if (playerMovement.isJumping())
velocity.y += 1; velocity.y += 1;
if (playerMovement.isSneaking()) if (playerMovement.isSneaking())
velocity.y -= 1; velocity.y -= 1;
return velocity.multiply(speedFactor); return velocity.multiplyInPlace(speedFactor);
} }
move(instanceVelocity) { move(instanceVelocity) {
@@ -82,10 +92,17 @@ export class FlexibleInstanceMove {
} }
moveOutline() { moveOutline() {
const { min, max } = this.outlineBounds();
this.outliner.setBounds(this.instance.getDimension(), min, max);
}
outlineBounds() {
const bounds = this.instance.getBounds(); const bounds = this.instance.getBounds();
const minWorldLocation = this.currentInstanceLocation.floor() const origin = this.currentInstanceLocation.floor();
const maxWorldLocation = minWorldLocation.add(Vector.from(bounds.max)); return {
this.outliner.setVertices(this.instance.getDimension(), minWorldLocation, maxWorldLocation); min: origin.add(bounds.min),
max: origin.add(bounds.max)
};
} }
onPlayerUseItem(event) { onPlayerUseItem(event) {
@@ -101,7 +118,7 @@ export class FlexibleInstanceMove {
finish() { finish() {
system.clearRun(this.runner); system.clearRun(this.runner);
this.outliner.stopDraw(); this.outliner.stop();
this.outliner = void 0; this.outliner = void 0;
this.instance.move(this.instance.getDimension().id, this.currentInstanceLocation); this.instance.move(this.instance.getDimension().id, this.currentInstanceLocation);
this.instance.enable(); this.instance.enable();
@@ -0,0 +1,130 @@
import { InstanceExistsError } from '../Errors/InstanceExistsError';
import { InstanceNotFoundError } from '../Errors/InstanceNotFoundError';
import { StructureNotFoundError } from '../Errors/StructureNotFoundError';
import { InstanceOptions } from './InstanceOptions';
import { StructureInstance } from './StructureInstance';
import { InvalidStructureError, world } from '@minecraft/server';
class InstanceCollection {
instances;
constructor() {
this.instances = {};
}
loadExistingInstances() {
world.getDynamicPropertyIds().filter(id => id.startsWith('instanceOptions:')).forEach(id => {
const instanceName = id.replace('instanceOptions:', '');
let structureId;
try {
structureId = InstanceOptions.getInstanceStructureId(instanceName);
this.instances[instanceName] = new StructureInstance(instanceName, structureId);
} catch (e) {
world.sendMessage(`§c[Construct] Error loading structure instance '${instanceName}'. It will be removed.`);
world.setDynamicProperty(id, void 0);
throw e;
}
});
}
add(instanceName, structureId, defaults = {}) {
if (this.instances[instanceName])
throw new InstanceExistsError(instanceName);
const instance = new StructureInstance(instanceName, structureId, defaults);
this.instances[instanceName] = instance;
return instance;
}
get(instanceName) {
const instance = this.instances[instanceName];
if (!instance)
throw new InstanceNotFoundError(instanceName);
return instance;
}
has(instanceName) {
return Boolean(this.instances[instanceName]);
}
delete(instanceName) {
const instance = this.get(instanceName);
instance.delete();
delete this.instances[instanceName];
}
getInstanceNames() {
return Object.keys(this.instances).sort((a, b) => a.localeCompare(b));
}
getInstanceNamesSortedByEnabled() {
const instanceNames = this.getInstanceNames();
instanceNames.sort((a, b) => {
const aEnabled = this.get(a).isEnabled();
const bEnabled = this.get(b).isEnabled();
if (aEnabled === bEnabled)
return a.localeCompare(b);
return bEnabled ? 1 : -1;
});
return instanceNames;
}
getInstancesAt(dimensionId, location, options = {}) {
return Object.values(this.instances).filter(instance => {
try {
return instance.isLocationActive(dimensionId, instance.toStructureCoords(location), options)
} catch (error) {
if (error instanceof StructureNotFoundError || error instanceof InvalidStructureError) {
this.delete(instance.name);
return false;
} else {
throw error;
}
}
});
}
getInstanceAt(dimensionId, location, options = {}) {
return this.getInstancesAt(dimensionId, location, options)[0];
}
fetchStructureBlock(dimensionId, location) {
const instance = this.getInstanceAt(dimensionId, location);
if (!instance)
return void 0;
return instance.getBlock(instance.toStructureCoords(location));
}
getWorldStructureIds() {
const structureManager = world.structureManager;
const packIds = [...new Set(structureManager.getPackStructureIds()
.map(id => id.replace('mystructure:', '')))
];
const packIdSet = new Set(packIds);
let worldStructureIds = [];
try {
worldStructureIds = structureManager.getWorldStructureIds()
.filter(id => id.startsWith('mystructure:'))
.map(id => id.replace('mystructure:', ''))
.filter(id => !packIdSet.has(id));
} catch (error) {
console.warn('Failed to fetch world structure IDs. They will be ignored. Error:', error);
}
return [...packIds, ...worldStructureIds];
}
rename(instanceName, newName) {
const structure = this.get(instanceName);
if (this.instances[newName])
throw new InstanceExistsError(newName);
structure.rename(newName);
this.instances[newName] = structure;
delete this.instances[instanceName];
structure.name = newName;
}
}
export const instanceCollection = new InstanceCollection();
world.afterEvents.worldLoad.subscribe(() => {
instanceCollection.loadExistingInstances();
});
@@ -1,4 +1,4 @@
import { structureCollection } from '../Structure/StructureCollection'; import { instanceCollection } from './InstanceCollection';
import { MenuForm } from '../MenuForm'; import { MenuForm } from '../MenuForm';
import { forceShow } from '../../utils'; import { forceShow } from '../../utils';
import { InstanceButtons } from '../Enums/InstanceButtons'; import { InstanceButtons } from '../Enums/InstanceButtons';
@@ -6,6 +6,7 @@ import { InstanceFormBuilder } from './InstanceFormBuilder';
import { FormCancelationReason } from '@minecraft/server-ui'; import { FormCancelationReason } from '@minecraft/server-ui';
import { FlexibleInstanceMove } from './FlexibleInstanceMove'; import { FlexibleInstanceMove } from './FlexibleInstanceMove';
import { Builders } from '../Builder/Builders'; import { Builders } from '../Builder/Builders';
import { RENDER_MODE_ORDER, RenderMode } from '../Enums/RenderMode';
export class InstanceForm { export class InstanceForm {
instanceName; instanceName;
@@ -38,7 +39,7 @@ export class InstanceForm {
constructor(player, instanceName) { constructor(player, instanceName) {
this.player = player; this.player = player;
this.instanceName = instanceName; this.instanceName = instanceName;
this.instance = structureCollection.get(this.instanceName); this.instance = instanceCollection.get(this.instanceName);
this.show(); this.show();
} }
@@ -84,7 +85,7 @@ export class InstanceForm {
this.renameInstanceForm(); this.renameInstanceForm();
break; break;
case InstanceButtons.Delete: case InstanceButtons.Delete:
structureCollection.delete(this.instanceName); instanceCollection.delete(this.instanceName);
break; break;
case InstanceButtons.NextLayer: case InstanceButtons.NextLayer:
this.instance.increaseLayer(); this.instance.increaseLayer();
@@ -128,7 +129,7 @@ export class InstanceForm {
return; return;
} }
try { try {
structureCollection.rename(this.instanceName, newName); instanceCollection.rename(this.instanceName, newName);
this.instanceName = newName; this.instanceName = newName;
} catch (e) { } catch (e) {
this.player.sendMessage({ translate: 'construct.instance.rename.error', with: [e.message] }); this.player.sendMessage({ translate: 'construct.instance.rename.error', with: [e.message] });
@@ -176,6 +177,8 @@ export class InstanceForm {
return; return;
this.instance.setVerifierEnabled(response.formValues[0]); this.instance.setVerifierEnabled(response.formValues[0]);
this.instance.setLayer(parseInt(response.formValues[1])); this.instance.setLayer(parseInt(response.formValues[1]));
this.instance.setRenderMode(RENDER_MODE_ORDER[response.formValues[2]] ?? RenderMode.Default);
this.instance.setVerifierRefreshSeconds(parseInt(response.formValues[3]));
}); });
} }
@@ -1,8 +1,10 @@
import { ActionFormData, ModalFormData } from '@minecraft/server-ui'; import { ActionFormData, ModalFormData } from '@minecraft/server-ui';
import { MenuFormBuilder } from '../MenuFormBuilder'; import { MenuFormBuilder } from '../MenuFormBuilder';
import { StructureVerifier } from '../Verifier/StructureVerifier'; import { StructureVerifier } from '../Verifier/StructureVerifier';
import { StructureStatistics } from '../Structure/StructureStatistics'; import { InstanceStatistics } from './InstanceStatistics';
import {EntityComponentTypes, TicksPerSecond, world} from '@minecraft/server'; import {EntityComponentTypes, TicksPerSecond, world} from '@minecraft/server';
import { RENDER_MODE_LABELS, RENDER_MODE_ORDER } from '../Enums/RenderMode';
import { RefreshRate } from '../Verifier/RefreshRate';
export class InstanceFormBuilder { export class InstanceFormBuilder {
static structureVerifier; static structureVerifier;
@@ -43,9 +45,9 @@ export class InstanceFormBuilder {
.title(MenuFormBuilder.menuTitle) .title(MenuFormBuilder.menuTitle)
if (this.structureVerifier) if (this.structureVerifier)
throw new Error('StructureVerifier is already running.'); throw new Error('StructureVerifier is already running.');
this.structureVerifier = new StructureVerifier(instance, { isEnabled: true, particleLifetime: 1*TicksPerSecond, isStandalone: true }); this.structureVerifier = StructureVerifier.standalone(instance, { particleLifetime: 1*TicksPerSecond });
const verification = await this.structureVerifier.verifyStructure(true); const verification = await this.structureVerifier.verifyStructure(true);
const statistics = new StructureStatistics(instance, verification); const statistics = new InstanceStatistics(instance, verification);
const statsMessage = statistics.getMessage(); const statsMessage = statistics.getMessage();
this.structureVerifier = void 0; this.structureVerifier = void 0;
buildStatisticsForm.body(statsMessage); buildStatisticsForm.body(statsMessage);
@@ -57,6 +59,24 @@ export class InstanceFormBuilder {
.title(MenuFormBuilder.menuTitle) .title(MenuFormBuilder.menuTitle)
.toggle({ translate: 'construct.instance.option.validation' }, { defaultValue: instance.options.verifier.isEnabled, tooltip: { translate: 'construct.instance.option.validation.description' }}) .toggle({ translate: 'construct.instance.option.validation' }, { defaultValue: instance.options.verifier.isEnabled, tooltip: { translate: 'construct.instance.option.validation.description' }})
.slider({ translate: 'construct.instance.option.layer'}, 0, instance.getMaxLayer(), { defaultValue: instance.getLayer(), valueStep: 1, tooltip: { translate: 'construct.instance.option.layer.description' }}) .slider({ translate: 'construct.instance.option.layer'}, 0, instance.getMaxLayer(), { defaultValue: instance.getLayer(), valueStep: 1, tooltip: { translate: 'construct.instance.option.layer.description' }})
.dropdown(
{ translate: 'construct.instance.option.rendermode' },
RENDER_MODE_ORDER.map(mode => ({ translate: RENDER_MODE_LABELS[mode] })),
{
defaultValueIndex: Math.max(RENDER_MODE_ORDER.indexOf(instance.options.renderMode), 0),
tooltip: { translate: 'construct.instance.option.rendermode.description' }
}
)
.slider(
{ translate: 'construct.instance.option.refreshrate' },
RefreshRate.MIN_SECONDS,
RefreshRate.MAX_SECONDS,
{
defaultValue: instance.options.verifier.refreshSeconds,
valueStep: RefreshRate.SECONDS_STEP,
tooltip: { translate: 'construct.instance.option.refreshrate.description' }
}
)
.submitButton({ translate: 'construct.menu.submit' }); .submitButton({ translate: 'construct.menu.submit' });
} }
@@ -1,6 +1,15 @@
import { Vector } from "../../lib/Vector"; import { Vector } from "../../lib/Vector";
import { world } from "@minecraft/server"; import { world } from "@minecraft/server";
import { Option } from "../Option"; import { Option } from "../Option";
import { RenderMode } from "../Enums/RenderMode";
import { RefreshRate } from "../Verifier/RefreshRate";
const VERIFIER_DEFAULTS = Object.freeze({
isEnabled: true,
trackPlayerDistance: 5,
particleLifetime: 10,
refreshSeconds: RefreshRate.DEFAULT_SECONDS
});
export class InstanceOptions extends Option { export class InstanceOptions extends Option {
#DP_NAMESPACE = "instanceOptions"; #DP_NAMESPACE = "instanceOptions";
@@ -10,25 +19,28 @@ export class InstanceOptions extends Option {
dimensionId = void 0; dimensionId = void 0;
worldLocation = new Vector(); worldLocation = new Vector();
currentLayer = 0; currentLayer = 0;
verifier = { verifier = { ...VERIFIER_DEFAULTS };
isEnabled: true, renderMode = RenderMode.Default;
trackPlayerDistance: 5,
particleLifetime: 10
};
static getInstanceStructureId(instanceName) { static getInstanceStructureId(instanceName) {
const options = new InstanceOptions(instanceName, void 0); const options = new InstanceOptions(instanceName, void 0);
return options.structureId; return options.structureId;
} }
constructor(instanceName, structureId) { constructor(instanceName, structureId, defaults = {}) {
super(); super();
this.instanceName = instanceName; this.instanceName = instanceName;
this.structureId = structureId; this.structureId = structureId;
this.load(); this.load();
this.#applyDefaults(defaults);
this.save(); this.save();
} }
#applyDefaults({ renderMode }) {
if (renderMode !== void 0)
this.renderMode = renderMode;
}
save() { save() {
this.saveToDP(this.#DP_NAMESPACE, this.instanceName, this); this.saveToDP(this.#DP_NAMESPACE, this.instanceName, this);
} }
@@ -36,6 +48,7 @@ export class InstanceOptions extends Option {
load() { load() {
this.loadFromDP(this.#DP_NAMESPACE, this.instanceName); this.loadFromDP(this.#DP_NAMESPACE, this.instanceName);
this.worldLocation = Vector.from(this.worldLocation); this.worldLocation = Vector.from(this.worldLocation);
this.verifier = { ...VERIFIER_DEFAULTS, ...this.verifier };
} }
clear() { clear() {
@@ -78,8 +91,18 @@ export class InstanceOptions extends Option {
this.save(); this.save();
} }
setVerifierRefreshSeconds(seconds) {
this.verifier.refreshSeconds = RefreshRate.clampSeconds(seconds);
this.save();
}
setVerifierParticleLifetime(lifetime) { setVerifierParticleLifetime(lifetime) {
this.verifier.particleLifetime = lifetime; this.verifier.particleLifetime = lifetime;
this.save(); this.save();
} }
setRenderMode(mode) {
this.renderMode = mode;
this.save();
}
} }
@@ -0,0 +1,46 @@
import { Vector } from '../../lib/Vector.js';
import { BlockVerificationLevel } from '../Enums/BlockVerificationLevel.js';
export class InstanceStatistics {
constructor(instance, verification) {
this.instance = instance;
this.statistics = verification.countByLevel();
}
getNonAirBlocks() {
const activeBounds = this.instance.getActiveBounds();
return Vector.volume(activeBounds.min, activeBounds.max) - this.statistics[BlockVerificationLevel.Air];
}
getStat(blockVerificationLevel) {
return { num: this.statistics[blockVerificationLevel], percent: this.statistics[blockVerificationLevel] / (this.getNonAirBlocks()) * 100 };
}
getSkipped() {
return this.statistics[BlockVerificationLevel.Skipped] || 0;
}
getMessage() {
const message = { rawtext: [] };
message.rawtext.push({ translate: 'construct.structure.statistics.header', with: [this.instance.getName()] });
if (this.instance.hasLayerSelected())
message.rawtext.push({ translate: 'construct.instance.materials.layer', with: [String(this.instance.getLayer())] });
message.rawtext.push({ rawtext: [
{ text: '\n' },
{ translate: 'construct.structure.statistics.blocks', with: [String(this.getNonAirBlocks())] },
{ text: '\n' }
]});
const skipped = this.getSkipped();
if (skipped > 0)
message.rawtext.push({ rawtext: [{ translate: 'construct.structure.statistics.skipped', with: [String(skipped)] }, { text: '\n' }] });
message.rawtext.push({ rawtext: [{ translate: 'construct.structure.statistics.correct', with: [this.formatStat(this.getStat(BlockVerificationLevel.Match))] }, { text: '\n' }] });
message.rawtext.push({ rawtext: [{ translate: 'construct.structure.statistics.stateincorrect', with: [this.formatStat(this.getStat(BlockVerificationLevel.TypeMatch))] }, { text: '\n' }] });
message.rawtext.push({ rawtext: [{ translate: 'construct.structure.statistics.incorrect', with: [this.formatStat(this.getStat(BlockVerificationLevel.NoMatch))] }, { text: '\n' }] });
message.rawtext.push({ rawtext: [{ translate: 'construct.structure.statistics.missing', with: [this.formatStat(this.getStat(BlockVerificationLevel.Missing))] }, { text: '\n' }] });
return message;
}
formatStat(stat) {
return `${stat.num} (${stat.percent.toFixed(2)}%%)`;
}
}
@@ -1,13 +1,13 @@
import { Vector } from "../../lib/Vector"; import { Vector } from "../../lib/Vector";
import { StructureOutliner } from "../Render/StructureOutliner"; import { StructureOutliner } from "../Render/outline/StructureOutliner";
import { StructureVerifier } from "../Verifier/StructureVerifier"; import { StructureVerifier } from "../Verifier/StructureVerifier";
import { Structure } from "../Structure/Structure"; import { Structure } from "../Structure/Structure";
import { InstanceOptions } from "./InstanceOptions"; import { InstanceOptions } from "./InstanceOptions";
import { world, system, TicksPerSecond } from "@minecraft/server"; import { world, system, TicksPerSecond } from "@minecraft/server";
import { InstanceNotPlacedError } from "../Errors/InstanceNotPlacedError"; import { InstanceNotPlacedError } from "../Errors/InstanceNotPlacedError";
import { StructureMaterials } from "../Materials/StructureMaterials"; import { StructureMaterials } from "../Materials/StructureMaterials";
import { VerificationRenderer } from "../Render/VerificationRenderer"; import { BlockPreviewRenderer } from "../Render/preview/BlockPreviewRenderer";
import { structureCollection } from "../Structure/StructureCollection"; import { instanceCollection } from "../Instance/InstanceCollection";
import { InstanceExistsError } from "../Errors/InstanceExistsError"; import { InstanceExistsError } from "../Errors/InstanceExistsError";
export class StructureInstance { export class StructureInstance {
@@ -15,13 +15,15 @@ export class StructureInstance {
structure = void 0; structure = void 0;
outliner = void 0; outliner = void 0;
verifier = void 0; verifier = void 0;
verificationRenderer = void 0; previewRenderer = void 0;
materials = void 0; materials = void 0;
flexMovingPlayerId = void 0; flexMovingPlayerId = void 0;
constructor(instanceName, structureId) { #cachedDimension = void 0;
constructor(instanceName, structureId, defaults = {}) {
this.structure = new Structure(structureId); this.structure = new Structure(structureId);
this.options = new InstanceOptions(instanceName, structureId); this.options = new InstanceOptions(instanceName, structureId, defaults);
this.refreshBox(); this.refreshBox();
this.subscribeToEvents(); this.subscribeToEvents();
} }
@@ -33,7 +35,7 @@ export class StructureInstance {
delete this.structure; delete this.structure;
delete this.outliner; delete this.outliner;
delete this.verifier; delete this.verifier;
delete this.verificationRenderer; delete this.previewRenderer;
delete this.materials; delete this.materials;
} }
@@ -43,14 +45,14 @@ export class StructureInstance {
if (!this.outliner) if (!this.outliner)
this.outliner = new StructureOutliner(this); this.outliner = new StructureOutliner(this);
if (!this.verifier) if (!this.verifier)
this.verifier = new StructureVerifier(this, { isEnabled: this.options.verifier.isEnabled }); this.verifier = StructureVerifier.forInstance(this);
if (!this.verificationRenderer) if (!this.previewRenderer)
this.verificationRenderer = new VerificationRenderer(this); this.previewRenderer = new BlockPreviewRenderer(this);
if (!this.materials) if (!this.materials)
this.materials = new StructureMaterials(this); this.materials = new StructureMaterials(this);
this.outliner.refresh(); this.outliner.refresh();
this.verifier.refresh(); this.verifier.refresh(true);
this.verificationRenderer.refresh(); this.previewRenderer.refresh(true);
this.materials.refresh(); this.materials.refresh();
} }
@@ -71,7 +73,9 @@ export class StructureInstance {
} }
getDimension() { getDimension() {
return this.options?.getDimension(); if (!this.#cachedDimension)
this.#cachedDimension = this.options?.getDimension();
return this.#cachedDimension;
} }
getLayer() { getLayer() {
@@ -79,7 +83,7 @@ export class StructureInstance {
} }
getMaxLayer() { getMaxLayer() {
return this.structure.getHeight(); return this.structure?.getHeight();
} }
getBounds() { getBounds() {
@@ -103,8 +107,8 @@ export class StructureInstance {
const min = this.structure.getMin(); const min = this.structure.getMin();
const max = this.structure.getMax(); const max = this.structure.getMax();
return { return {
min: new Vector(min.x, layer - 1, min.z), min: { x: min.x, y: layer - 1, z: min.z },
max: new Vector(max.x, layer, max.z) max: { x: max.x, y: layer, z: max.z }
}; };
} }
@@ -197,7 +201,7 @@ export class StructureInstance {
} }
rename(newName) { rename(newName) {
if (structureCollection.has(newName)) if (instanceCollection.has(newName))
throw new InstanceExistsError(newName); throw new InstanceExistsError(newName);
this.options.rename(newName); this.options.rename(newName);
} }
@@ -227,7 +231,7 @@ export class StructureInstance {
setVerifierEnabled(enable) { setVerifierEnabled(enable) {
this.options.setVerifierEnabled(enable); this.options.setVerifierEnabled(enable);
this.verifier.refresh(); this.verifier.refresh();
this.verificationRenderer.refresh(); this.previewRenderer.refresh();
} }
setVerifierDistance(distance) { setVerifierDistance(distance) {
@@ -238,7 +242,22 @@ export class StructureInstance {
} else { } else {
this.options.verifier.particleLifetime = 10; this.options.verifier.particleLifetime = 10;
} }
this.verificationRenderer.refresh(); this.previewRenderer.refresh();
}
setVerifierRefreshSeconds(seconds) {
this.options.setVerifierRefreshSeconds(seconds);
this.verifier.refresh();
this.previewRenderer.refresh();
}
getRenderMode() {
return this.options.renderMode;
}
setRenderMode(mode) {
this.options.setRenderMode(mode);
this.refreshBox();
} }
increaseLayer() { increaseLayer() {
@@ -280,11 +299,19 @@ export class StructureInstance {
} }
toGlobalCoords(structureLocation) { toGlobalCoords(structureLocation) {
return Vector.from(structureLocation).add(this.options.worldLocation); return {
x: structureLocation.x + this.options.worldLocation.x,
y: structureLocation.y + this.options.worldLocation.y,
z: structureLocation.z + this.options.worldLocation.z
};
} }
toStructureCoords(worldLocation) { toStructureCoords(worldLocation) {
return Vector.from(worldLocation).subtract(this.options.worldLocation); return {
x: worldLocation.x - this.options.worldLocation.x,
y: worldLocation.y - this.options.worldLocation.y,
z: worldLocation.z - this.options.worldLocation.z
};
} }
asPacket() { asPacket() {
@@ -302,14 +329,15 @@ export class StructureInstance {
isEnabled: this.options.verifier.isEnabled, isEnabled: this.options.verifier.isEnabled,
trackPlayerDistance: this.options.verifier.trackPlayerDistance, trackPlayerDistance: this.options.verifier.trackPlayerDistance,
particleLifetime: this.options.verifier.particleLifetime particleLifetime: this.options.verifier.particleLifetime
} },
renderMode: this.options.renderMode
}; };
} }
setOptions(newOptions) { setOptions(newOptions) {
const newVerifierOptions = newOptions.verifier; const newVerifierOptions = newOptions.verifier;
if (newOptions.name !== this.getName()) if (newOptions.name !== this.getName())
structureCollection.rename(this.getName(), newOptions.name); instanceCollection.rename(this.getName(), newOptions.name);
this.setStructure(newOptions.structureId); this.setStructure(newOptions.structureId);
this.options.setEnabled(newOptions.isEnabled); this.options.setEnabled(newOptions.isEnabled);
this.options.move(newOptions.dimensionId, newOptions.location); this.options.move(newOptions.dimensionId, newOptions.location);
@@ -317,6 +345,7 @@ export class StructureInstance {
this.options.setVerifierEnabled(newVerifierOptions.isEnabled); this.options.setVerifierEnabled(newVerifierOptions.isEnabled);
this.options.setVerifierDistance(newVerifierOptions.trackPlayerDistance); this.options.setVerifierDistance(newVerifierOptions.trackPlayerDistance);
this.options.setVerifierParticleLifetime(newVerifierOptions.particleLifetime); this.options.setVerifierParticleLifetime(newVerifierOptions.particleLifetime);
this.options.setRenderMode(newOptions.renderMode);
this.options.save(); this.options.save();
this.refreshBox(); this.refreshBox();
} }
@@ -1,7 +1,7 @@
import { MaterialGrabberFormBuilder } from './MaterialGrabberFormBuilder'; import { MaterialGrabberFormBuilder } from './MaterialGrabberFormBuilder';
import { forceShow } from '../../utils'; import { forceShow } from '../../utils';
import { Builders } from '../Builder/Builders'; import { Builders } from '../Builder/Builders';
import { structureCollection } from '../Structure/StructureCollection'; import { instanceCollection } from '../Instance/InstanceCollection';
export class MaterialGrabberForm { export class MaterialGrabberForm {
constructor(player) { constructor(player) {
@@ -11,10 +11,11 @@ export class MaterialGrabberForm {
show() { show() {
try { try {
return forceShow(this.player, MaterialGrabberFormBuilder.buildInstanceSelector(this.player)).then((response) => { const instanceNames = instanceCollection.getInstanceNamesSortedByEnabled();
return forceShow(this.player, MaterialGrabberFormBuilder.buildInstanceSelector(this.player, instanceNames)).then((response) => {
if (response.canceled) if (response.canceled)
return; return;
const selectedInstanceName = structureCollection.getInstanceNames()[response.selection]; const selectedInstanceName = instanceNames[response.selection];
if (selectedInstanceName) { if (selectedInstanceName) {
this.setActiveInstance(selectedInstanceName); this.setActiveInstance(selectedInstanceName);
this.player.sendMessage({ translate: 'construct.materials.grabber.menu.success', with: [selectedInstanceName] }); this.player.sendMessage({ translate: 'construct.materials.grabber.menu.success', with: [selectedInstanceName] });
@@ -1,12 +1,12 @@
import { ActionFormData } from "@minecraft/server-ui"; import { ActionFormData } from "@minecraft/server-ui";
import { MenuFormBuilder } from "../MenuFormBuilder"; import { MenuFormBuilder } from "../MenuFormBuilder";
import { structureCollection } from "../Structure/StructureCollection"; import { instanceCollection } from "../Instance/InstanceCollection";
import { Builders } from "../Builder/Builders"; import { Builders } from "../Builder/Builders";
export class MaterialGrabberFormBuilder { export class MaterialGrabberFormBuilder {
static menuTitle = { rawtext: [MenuFormBuilder.menuTitle, { translate: 'construct.materials.grabber.menu.title' }] }; static menuTitle = { rawtext: [MenuFormBuilder.menuTitle, { translate: 'construct.materials.grabber.menu.title' }] };
static buildInstanceSelector(player) { static buildInstanceSelector(player, instanceNames) {
const allInstanceNameForm = new ActionFormData() const allInstanceNameForm = new ActionFormData()
.title(this.menuTitle); .title(this.menuTitle);
const currInstanceName = Builders.get(player.id).materialInstanceName; const currInstanceName = Builders.get(player.id).materialInstanceName;
@@ -18,8 +18,8 @@ export class MaterialGrabberFormBuilder {
body.rawtext.push({ text: '\n' }); body.rawtext.push({ text: '\n' });
body.rawtext.push({ translate: 'construct.materials.grabber.menu.selectinstance' }); body.rawtext.push({ translate: 'construct.materials.grabber.menu.selectinstance' });
allInstanceNameForm.body(body); allInstanceNameForm.body(body);
structureCollection.getInstanceNames().forEach(instanceName => { instanceNames.forEach(instanceName => {
allInstanceNameForm.button(`§2${instanceName}`); allInstanceNameForm.button(`${instanceCollection.get(instanceName).isEnabled() ? '§2' : '§c'}${instanceName}`);
}); });
return allInstanceNameForm; return allInstanceNameForm;
} }
@@ -4,11 +4,11 @@ import { InstanceNotPlacedError } from "../Errors/InstanceNotPlacedError";
class StructureMaterials { class StructureMaterials {
instance; instance;
materials; #materials;
constructor(instance) { constructor(instance) {
this.instance = instance; this.instance = instance;
this.materials = {}; this.#materials = {};
} }
refresh() { refresh() {
@@ -31,26 +31,26 @@ class StructureMaterials {
} }
get materials() { get materials() {
return this.materials; return this.#materials;
} }
get(itemType) { get(itemType) {
return this.materials[itemType]; return this.#materials[itemType];
} }
isEmpty() { isEmpty() {
return Object.keys(this.materials).length === 0; return Object.keys(this.#materials).length === 0;
} }
has(itemType) { has(itemType) {
return this.materials[itemType] !== undefined; return this.#materials[itemType] !== undefined;
} }
remove(itemType, amount) { remove(itemType, amount) {
if (!this.materials[itemType]) return; if (!this.#materials[itemType]) return;
this.materials[itemType].count -= amount; this.#materials[itemType].count -= amount;
if (this.materials[itemType].count <= 0) if (this.#materials[itemType].count <= 0)
delete this.materials[itemType]; delete this.#materials[itemType];
} }
*populateAll() { *populateAll() {
@@ -78,21 +78,21 @@ class StructureMaterials {
} }
countBlock(block) { countBlock(block) {
const itemStack = block?.getItemStack(); const itemStack = block?.permutation.getItemStack();
const typeId = itemStack?.typeId; const typeId = itemStack?.typeId;
if (!typeId) return; if (!typeId) return;
if (!this.materials[typeId]) if (!this.#materials[typeId])
this.materials[typeId] = { count: 0, stackSize: itemStack.maxAmount }; this.#materials[typeId] = { count: 0, stackSize: itemStack.maxAmount };
this.materials[typeId].count++; this.#materials[typeId].count++;
} }
clear() { clear() {
for (const key in this.materials) for (const key in this.#materials)
delete this.materials[key]; delete this.#materials[key];
} }
formatString(otherMaterials = void 0) { formatString(otherMaterials = void 0) {
const materials = otherMaterials || this.materials; const materials = otherMaterials || this.#materials;
let message = { rawtext: [] }; let message = { rawtext: [] };
const sortedTypes = Object.keys(materials).sort( const sortedTypes = Object.keys(materials).sort(
(a, b) => materials[b].count - materials[a].count (a, b) => materials[b].count - materials[a].count
@@ -126,7 +126,7 @@ class StructureMaterials {
} }
getMaterialsDifference(container) { getMaterialsDifference(container) {
const missingMaterials = JSON.parse(JSON.stringify(this.materials)); const missingMaterials = JSON.parse(JSON.stringify(this.#materials));
for (let slotIndex = 0; slotIndex < container.size; slotIndex++) { for (let slotIndex = 0; slotIndex < container.size; slotIndex++) {
const slot = container.getSlot(slotIndex); const slot = container.getSlot(slotIndex);
if (slot.hasItem()) { if (slot.hasItem()) {
+11 -7
View File
@@ -1,10 +1,11 @@
import { forceShow } from '../utils'; import { forceShow } from '../utils';
import { structureCollection } from './Structure/StructureCollection'; import { instanceCollection } from './Instance/InstanceCollection';
import { MenuFormBuilder } from './MenuFormBuilder'; import { MenuFormBuilder } from './MenuFormBuilder';
import { InstanceForm } from './Instance/InstanceForm'; import { InstanceForm } from './Instance/InstanceForm';
import { BuilderForm } from './Builder/BuilderForm'; import { BuilderForm } from './Builder/BuilderForm';
import { InstanceExistsError } from './Errors/InstanceExistsError'; import { InstanceExistsError } from './Errors/InstanceExistsError';
import { StructureNotFoundError } from './Errors/StructureNotFoundError'; import { StructureNotFoundError } from './Errors/StructureNotFoundError';
import { getDefaultRenderMode } from '../options/defaultRenderMode';
export class MenuForm { export class MenuForm {
constructor(player, { jumpToInstance = false, instanceName = void 0 } = {}) { constructor(player, { jumpToInstance = false, instanceName = void 0 } = {}) {
@@ -15,7 +16,7 @@ export class MenuForm {
async show(jumpToInstance = false, instanceName = void 0) { async show(jumpToInstance = false, instanceName = void 0) {
if (jumpToInstance) { if (jumpToInstance) {
if (!instanceName) if (!instanceName)
instanceName = structureCollection.getStructure(this.player.dimension.id, this.player.location, { useActiveLayer: false })?.getName(); instanceName = instanceCollection.getInstanceAt(this.player.dimension.id, this.player.location, { useActiveLayer: false })?.getName();
if (instanceName) { if (instanceName) {
new InstanceForm(this.player, instanceName); new InstanceForm(this.player, instanceName);
return; return;
@@ -29,19 +30,20 @@ export class MenuForm {
async getInstanceNameFromForm() { async getInstanceNameFromForm() {
try { try {
return forceShow(this.player, MenuFormBuilder.buildAllInstanceName()).then((response) => { const instanceNames = instanceCollection.getInstanceNamesSortedByEnabled();
return forceShow(this.player, MenuFormBuilder.buildAllInstanceName(instanceNames)).then((response) => {
if (response.canceled) if (response.canceled)
return void 0; return void 0;
let selection = response.selection; let selection = response.selection;
if (selection === 0) { if (selection === 0) {
new BuilderForm(this.player); new BuilderForm(this.player);
return void 0; return void 0;
} else if (selection == structureCollection.getInstanceNames().length + 2) { } else if (selection == instanceNames.length + 2) {
MenuFormBuilder.buildHowTo().show(this.player); MenuFormBuilder.buildHowTo().show(this.player);
return void 0; return void 0;
} else { } else {
selection--; selection--;
const selectedInstanceName = structureCollection.getInstanceNames()[selection]; const selectedInstanceName = instanceNames[selection];
return selectedInstanceName || this.createNewInstance(); return selectedInstanceName || this.createNewInstance();
} }
}); });
@@ -65,7 +67,7 @@ export class MenuForm {
if (!structureId) if (!structureId)
return void 0; return void 0;
try { try {
structureCollection.add(instanceName, structureId); instanceCollection.add(instanceName, structureId, { renderMode: getDefaultRenderMode(this.player.id) });
} catch (error) { } catch (error) {
if (error instanceof InstanceExistsError || error instanceof StructureNotFoundError) { if (error instanceof InstanceExistsError || error instanceof StructureNotFoundError) {
error.sendTo(this.player); error.sendTo(this.player);
@@ -81,7 +83,9 @@ export class MenuForm {
return MenuFormBuilder.buildAllStructures().show(this.player).then((response) => { return MenuFormBuilder.buildAllStructures().show(this.player).then((response) => {
if (response.canceled) if (response.canceled)
return void 0; return void 0;
const selectedStructureId = structureCollection.getWorldStructureIds()[response.selection]; const worldStructureIds = instanceCollection.getWorldStructureIds();
worldStructureIds.sort((a, b) => a.localeCompare(b));
const selectedStructureId = worldStructureIds[response.selection];
return selectedStructureId || this.getOtherStructureId(); return selectedStructureId || this.getOtherStructureId();
}); });
} }
+7 -5
View File
@@ -1,16 +1,16 @@
import { ActionFormData, ModalFormData } from '@minecraft/server-ui'; import { ActionFormData, ModalFormData } from '@minecraft/server-ui';
import { structureCollection } from './Structure/StructureCollection'; import { instanceCollection } from './Instance/InstanceCollection';
export class MenuFormBuilder { export class MenuFormBuilder {
static menuTitle = { translate: 'construct.mainmenu.title' }; static menuTitle = { translate: 'construct.mainmenu.title' };
static buildAllInstanceName() { static buildAllInstanceName(instanceNames) {
const allInstanceNameForm = new ActionFormData() const allInstanceNameForm = new ActionFormData()
.title(this.menuTitle) .title(this.menuTitle)
.body({ translate: 'construct.mainmenu.selectinstance' }); .body({ translate: 'construct.mainmenu.selectinstance' });
allInstanceNameForm.button({ translate: 'construct.mainmenu.settings' }); allInstanceNameForm.button({ translate: 'construct.mainmenu.settings' });
structureCollection.getInstanceNames().forEach(instanceName => { instanceNames.forEach(instanceName => {
allInstanceNameForm.button(`${structureCollection.get(instanceName).isEnabled() ? '§2' : '§c'}${instanceName}`); allInstanceNameForm.button(`${instanceCollection.get(instanceName).isEnabled() ? '§2' : '§c'}${instanceName}`);
}); });
allInstanceNameForm.button({ translate: 'construct.mainmenu.newinstance' }); allInstanceNameForm.button({ translate: 'construct.mainmenu.newinstance' });
allInstanceNameForm.button({ translate: 'construct.mainmenu.howto' }); allInstanceNameForm.button({ translate: 'construct.mainmenu.howto' });
@@ -28,7 +28,9 @@ export class MenuFormBuilder {
const allStructuresForm = new ActionFormData() const allStructuresForm = new ActionFormData()
.title(this.menuTitle) .title(this.menuTitle)
.body({ translate: 'construct.mainmenu.selectstructure.header' }); .body({ translate: 'construct.mainmenu.selectstructure.header' });
structureCollection.getWorldStructureIds().forEach(structureId => { const worldStructureIds = instanceCollection.getWorldStructureIds();
worldStructureIds.sort((a, b) => a.localeCompare(b));
worldStructureIds.forEach(structureId => {
const structureName = structureId.replace('mystructure:', ''); const structureName = structureId.replace('mystructure:', '');
allStructuresForm.button(`§2${structureName}`); allStructuresForm.button(`§2${structureName}`);
}); });
@@ -0,0 +1,69 @@
import { world, system, EntityComponentTypes, EntitySwingSource, EquipmentSlot, HeldItemOption, GameMode } from '@minecraft/server';
import { MENU_ITEM } from '../consts';
import { MenuForm } from './MenuForm';
import { instanceCollection } from './Instance/InstanceCollection';
import { Builders } from './Builder/Builders';
import { StructurePickBlock } from "./Structure/StructurePickBlock";
class MenuItemInteractions {
static onItemUse(event) {
if (!event.source || !MenuItemInteractions.#isMenuItem(event.itemStack))
return;
event.cancel = true;
const builder = Builders.get(event.source.id);
system.run(() => {
if (builder.isFlexibleInstanceMoving())
return;
MenuItemInteractions.#openMenu(event.source, event);
});
}
static onPlayerBreakBlock(event) {
if (!event.player || !MenuItemInteractions.#isMenuItem(event.itemStack))
return;
event.cancel = true;
}
static onPlayerStartBreakingBlock(event) {
const player = event.player;
if (!player || !MenuItemInteractions.#isMenuItem(event.heldItemStack))
return;
StructurePickBlock.perform(player);
}
static onPlayerAttack(event) {
if (!event.player || !MenuItemInteractions.#isMenuItem(event.heldItemStack))
return;
StructurePickBlock.perform(event.player);
}
static #isMenuItem(itemStack) {
return itemStack?.typeId === MENU_ITEM;
}
static #openMenu(player, event = void 0) {
const options = { jumpToInstance: true };
if (event) {
const instanceNames = instanceCollection.getInstanceNames();
const instanceName = event.itemStack?.nameTag;
if (instanceNames.includes(instanceName))
options.instanceName = instanceName;
}
new MenuForm(player, options);
}
static #getMainhandItemStack(player) {
const equippableComponent = player.getComponent(EntityComponentTypes.Equippable);
if (!equippableComponent)
return;
const mainhandSlot = equippableComponent.getEquipmentSlot(EquipmentSlot.Mainhand);
if (!mainhandSlot)
return;
return mainhandSlot.getItem();
}
}
world.beforeEvents.itemUse.subscribe(MenuItemInteractions.onItemUse);
world.afterEvents.playerSwingStart.subscribe(MenuItemInteractions.onPlayerAttack, { heldItemOption: HeldItemOption.AnyItem, swingSource: EntitySwingSource.Attack });
world.afterEvents.playerStartBreakingBlock.subscribe(MenuItemInteractions.onPlayerStartBreakingBlock);
world.beforeEvents.playerBreakBlock.subscribe(MenuItemInteractions.onPlayerBreakBlock);
+6 -10
View File
@@ -1,22 +1,18 @@
import { structureCollection } from "./Structure/StructureCollection"; import { instanceCollection } from "./Instance/InstanceCollection";
export class Raycaster { export class Raycaster {
static STEP_SIZE = 0.2; static STEP_SIZE = 0.2;
static getStructureBlocks(dimension, startLocation, direction, { maxDistance = 7, getFirst = true, collideWithWorldBlocks = true, useActiveLayer = true }) { static getStructureBlocks(dimension, startLocation, direction, { maxDistance = 7, getFirst = true, collideWithWorldBlocks = true, useActiveLayer = true }) {
// Can probably be optimized by the fact that we only need full blocks and aren't checking for partial blocks
const blocks = []; const blocks = [];
let location = startLocation; let location = startLocation;
let distance = 0; let distance = 0;
while (distance < maxDistance) { while (distance < maxDistance) {
const structure = structureCollection.getStructure(dimension.id, location, { useActiveLayer }); const instance = instanceCollection.getInstanceAt(dimension.id, location, { useActiveLayer });
if (structure) { if (instance) {
const block = structure.getBlock(structure.toStructureCoords(location)); const block = instance.getBlock(instance.toStructureCoords(location));
if (block?.type.id !== 'minecraft:air') { if (block?.typeId !== 'minecraft:air') {
blocks.push({ blocks.push({ block, location, instance});
permutation: block,
location: location
});
if (getFirst) if (getFirst)
break; break;
} }
@@ -0,0 +1,74 @@
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel";
export const MaterialType = Object.freeze({
OPAQUE: "opaque",
BLEND: "blend"
});
// An incorrect block is drawn slightly outside its cell so it wraps the real
// block instead of z-fighting with it.
const OVERLAY_SCALE = 1.01;
// A missing block is drawn slightly inside its cell so a run of them reads as one mass,
// but when a block is placed there is no z-fighting. This inset is also the one thing
// keeping neighbor culling from being exactly invisible: two missing blocks
// side by side stop short of their shared boundary, so culling the faces they
// present to each other opens a seam visible at a grazing angle. Setting this
// to 1.00 makes every cull exact, but causes z-fighting for placed blocks.
const MISSING_SCALE = 0.995;
// A missing block in a mode that draws no preview (the Classic look): a small see-through cube
// floating in the middle of its cell.
const MARKER_SCALE = 0.90;
const PARTICLE_STYLES = Object.freeze({
[BlockVerificationLevel.NoMatch]: Object.freeze({
color: { red: 1, green: 0, blue: 0, alpha: 0.2 }, scale: OVERLAY_SCALE, material: MaterialType.BLEND
}),
[BlockVerificationLevel.TypeMatch]: Object.freeze({
color: { red: 1, green: 1, blue: 0, alpha: 0.2 }, scale: OVERLAY_SCALE, material: MaterialType.BLEND
}),
[BlockVerificationLevel.Missing]: Object.freeze({
color: { red: 0.55, green: 0.8, blue: 1, alpha: 1 }, scale: MISSING_SCALE, material: MaterialType.OPAQUE
})
});
const MARKER_STYLE = Object.freeze({
color: { red: 0, green: 0, blue: 1, alpha: 0.2 }, scale: MARKER_SCALE, material: MaterialType.BLEND
});
const BOX_STYLES = Object.freeze({
[BlockVerificationLevel.NoMatch]: Object.freeze({
color: { red: 1, green: 0, blue: 0, alpha: 1 }, scale: OVERLAY_SCALE
}),
[BlockVerificationLevel.TypeMatch]: Object.freeze({
color: { red: 1, green: 1, blue: 0, alpha: 1 }, scale: OVERLAY_SCALE
}),
[BlockVerificationLevel.Missing]: Object.freeze({
color: { red: 0.3, green: 0.57, blue: 0.87, alpha: 1 }, scale: 1.00
})
});
// A face the pipeline couldn't resolve.
export const UNRESOLVED_FACE_STYLE = Object.freeze({
color: { red: 0.3, green: 0.57, blue: 0.87, alpha: 0.2 }, material: MaterialType.BLEND
});
// Water is the one block whose texture is authored see-through - Java draws it
// translucent and block/water_still carries an alpha of 180/255, which the
// atlas keeps. The preview's usual material alpha-tests rather than blending,
// which forced every partly-transparent texel solid.
export const WATER_MATERIAL = MaterialType.BLEND;
// Undefined for levels that draw no particle at all.
export function particleStyleOf(verificationLevel, showsBlockPreview) {
if (verificationLevel === BlockVerificationLevel.Missing && !showsBlockPreview)
return MARKER_STYLE;
return PARTICLE_STYLES[verificationLevel];
}
// Undefined for levels that get no box. That is what makes a cell going to
// Match or Air a removal rather than a recolour.
export function boxStyleOf(verificationLevel) {
return BOX_STYLES[verificationLevel];
}
@@ -0,0 +1,36 @@
import { blockKeySpecs, blockModels } from "../../../blockModels";
export class BlockModelKeyResolver {
lookupFaceRefs(blockId, states) {
const keySpec = blockKeySpecs[blockId];
if (keySpec === void 0)
return void 0;
for (let shapeIndex = 0; shapeIndex < keySpec.props.length; shapeIndex++) {
const key = this.buildStateKey(blockId, keySpec.props[shapeIndex], shapeIndex, states);
if (key === void 0)
continue;
const faceReferences = blockModels[key];
if (faceReferences !== void 0)
return faceReferences;
}
return void 0;
}
faceRefsForKey(key) {
return blockModels[key];
}
buildStateKey(blockId, shapeProperties, shapeIndex, states) {
let key = `${blockId}[${shapeIndex}|`;
for (let propertyIndex = 0; propertyIndex < shapeProperties.length; propertyIndex++) {
const propertyName = shapeProperties[propertyIndex];
const value = states[propertyName];
if (value === void 0)
return void 0;
if (propertyIndex > 0)
key += ",";
key += typeof value === "boolean" ? (value ? 1 : 0) : value;
}
return `${key}]`;
}
}
@@ -0,0 +1,96 @@
import { FaceTable } from "./FaceTable";
import { CubeFaceLibrary } from "./CubeFaceLibrary";
import { BlockModelKeyResolver } from "./BlockModelKeyResolver";
import { CellFlags } from "../../Verifier/CellFlags";
class BlockModelResolver {
#waterBlockIds = new Set(["minecraft:water", "minecraft:flowing_water"]);
#waterloggedWaterState = "minecraft:water[0|0]";
constructor({ keyResolver, faceReader, unknownFaces, sideMaskPacker }) {
this.keyResolver = keyResolver;
this.faceReader = faceReader;
this.unknownFaces = unknownFaces;
this.sideMaskPacker = sideMaskPacker;
this.resolvedByBlock = new WeakMap();
this.waterloggedFacesCache = void 0;
this.overlaySideMasksCache = void 0;
}
facesOf(block) {
return this.resolve(block).faces;
}
sideMasksOf(block) {
return this.resolve(block).sideMasks;
}
overlaySideMasks() {
if (this.overlaySideMasksCache === void 0)
this.overlaySideMasksCache = this.computeSideMasks(this.unknownFaces);
return this.overlaySideMasksCache;
}
waterloggedFaces() {
if (this.waterloggedFacesCache === void 0) {
const faceRefs = this.keyResolver.faceRefsForKey(this.#waterloggedWaterState) ?? [];
this.waterloggedFacesCache = faceRefs.map(this.faceReader);
}
return this.waterloggedFacesCache;
}
isWater(block) {
return this.#waterBlockIds.has(this.blockIdOf(block));
}
resolve(block) {
const cached = this.resolvedByBlock.get(block);
if (cached !== void 0)
return cached;
const faces = this.lookupFaces(block);
const resolved = {
faces,
sideMasks: this.computeSideMasks(faces)
};
this.resolvedByBlock.set(block, resolved);
return resolved;
}
lookupFaces(block) {
const blockId = this.blockIdOf(block);
const states = block.states ?? block.getAllStates();
const faceReferences = this.keyResolver.lookupFaceRefs(blockId, states);
if (faceReferences === void 0)
return this.unknownFaces;
return faceReferences.map(this.faceReader);
}
computeSideMasks(faces) {
let markerMask = 0;
let opaqueMask = 0;
for (const face of faces) {
if (!face.covers)
continue;
if (face.missing)
markerMask |= 1 << face.cull;
else if (face.opaque)
opaqueMask |= 1 << face.cull;
}
return this.sideMaskPacker(markerMask, opaqueMask);
}
blockIdOf(block) {
return block.typeId ?? block.type.id;
}
}
export const blockModelResolver = new BlockModelResolver({
keyResolver: new BlockModelKeyResolver(),
faceReader: FaceTable.faceAt,
unknownFaces: CubeFaceLibrary.createPlainUnknownCubeFaces(),
sideMaskPacker: CellFlags.pack
});
@@ -0,0 +1,23 @@
import { whiteUvRect } from "../../../blockAtlas";
export class CubeFaceLibrary {
static createPlainCubeFaces() {
return [
{ center: [8, 16, 8], width: 16, height: 16, facing: [0, -1, 0], roll: 180, tintindex: -1, uv: whiteUvRect, cull: 1, covers: true },
{ center: [8, 0, 8], width: 16, height: 16, facing: [0, 1, 0], roll: 0, tintindex: -1, uv: whiteUvRect, cull: 0, covers: true },
{ center: [8, 8, 0], width: 16, height: 16, facing: [0, 0, -1], roll: 0, tintindex: -1, uv: whiteUvRect, cull: 2, covers: true },
{ center: [8, 8, 16], width: 16, height: 16, facing: [0, 0, 1], roll: 0, tintindex: -1, uv: whiteUvRect, cull: 3, covers: true },
{ center: [16, 8, 8], width: 16, height: 16, facing: [1, 0, 0], roll: 0, tintindex: -1, uv: whiteUvRect, cull: 5, covers: true },
{ center: [0, 8, 8], width: 16, height: 16, facing: [-1, 0, 0], roll: 0, tintindex: -1, uv: whiteUvRect, cull: 4, covers: true }
];
}
static createUnknownCubeFaces(baseFaces) {
return baseFaces.map((face) => ({ ...face, missing: true }));
}
static createPlainUnknownCubeFaces() {
const plainFaces = CubeFaceLibrary.createPlainCubeFaces();
return CubeFaceLibrary.createUnknownCubeFaces(plainFaces);
}
}
@@ -0,0 +1,76 @@
import { blockFaceData, blockMissingFaces } from "../../../blockModels";
export class FaceTable {
static FACE_CULL = 0;
static FACE_FACING = 1;
static FACE_CENTER = 4;
static FACE_WIDTH = 7;
static FACE_HEIGHT = 8;
static FACE_ROLL = 9;
static FACE_TINTINDEX = 10;
static FACE_UV = 11;
static FACE_ROW_WIDTH = 15;
static NO_CULL = -1;
// The cull column carries two extra bits above the direction: whether the face spans its whole side of the block, and whether it does so opaquely.
static FACE_CULL_MASK = 0b111;
static FACE_COVERS_SIDE = 0b1000;
static FACE_OPAQUE_SIDE = 0b10000;
static #faceCount = blockFaceData.length / FaceTable.FACE_ROW_WIDTH;
static #missingFaces = new Set(blockMissingFaces);
static #materialized = new Array(FaceTable.#faceCount);
static faceAt(index) {
const cachedFace = FaceTable.#materialized[index];
if (cachedFace !== void 0)
return cachedFace;
const face = FaceTable.readFaceRow(index * FaceTable.FACE_ROW_WIDTH);
if (FaceTable.#missingFaces.has(index))
face.missing = true;
FaceTable.#materialized[index] = face;
return face;
}
static readFaceRow(offset) {
const face = {
center: [
blockFaceData[offset + FaceTable.FACE_CENTER],
blockFaceData[offset + FaceTable.FACE_CENTER + 1],
blockFaceData[offset + FaceTable.FACE_CENTER + 2]
],
width: blockFaceData[offset + FaceTable.FACE_WIDTH],
height: blockFaceData[offset + FaceTable.FACE_HEIGHT],
facing: [
blockFaceData[offset + FaceTable.FACE_FACING],
blockFaceData[offset + FaceTable.FACE_FACING + 1],
blockFaceData[offset + FaceTable.FACE_FACING + 2]
],
roll: blockFaceData[offset + FaceTable.FACE_ROLL],
tintindex: blockFaceData[offset + FaceTable.FACE_TINTINDEX],
uv: {
x: blockFaceData[offset + FaceTable.FACE_UV],
y: blockFaceData[offset + FaceTable.FACE_UV + 1],
w: blockFaceData[offset + FaceTable.FACE_UV + 2],
h: blockFaceData[offset + FaceTable.FACE_UV + 3]
}
};
FaceTable.applyCullFlags(face, blockFaceData[offset + FaceTable.FACE_CULL]);
return face;
}
static applyCullFlags(face, cullData) {
if (cullData === FaceTable.NO_CULL)
return;
face.cull = cullData & FaceTable.FACE_CULL_MASK;
if (cullData & FaceTable.FACE_COVERS_SIDE)
face.covers = true;
if (cullData & FaceTable.FACE_OPAQUE_SIDE)
face.opaque = true;
}
}
@@ -0,0 +1,66 @@
import { Vector } from "../../../lib/Vector.js";
export class Cuboid {
static #edges = [
[0, 1], [0, 2], [0, 4], [1, 3],
[1, 5], [2, 3], [2, 6], [3, 7],
[4, 5], [4, 6], [5, 7], [6, 7]
];
static #maxSegmentsPerEdge = 16;
constructor(min, max) {
this.min = Vector.from(min);
this.max = Vector.from(max);
this.corners = Cuboid.cornersOf(this.min, this.max);
}
static cornersOf(min, max) {
return [
new Vector(min.x, min.y, min.z),
new Vector(max.x, min.y, min.z),
new Vector(min.x, max.y, min.z),
new Vector(max.x, max.y, min.z),
new Vector(min.x, min.y, max.z),
new Vector(max.x, min.y, max.z),
new Vector(min.x, max.y, max.z),
new Vector(max.x, max.y, max.z)
];
}
static segmentCount(start, end) {
return Math.min(Math.floor(end.distance(start)), Cuboid.#maxSegmentsPerEdge);
}
static *edgePoints(corners) {
for (const [startIndex, endIndex] of Cuboid.#edges) {
const start = corners[startIndex];
const end = corners[endIndex];
const segments = Cuboid.segmentCount(start, end);
for (let i = 1; i < segments; i++)
yield start.lerp(end, i / segments);
}
}
static *edgeSegments(corners) {
for (const [startIndex, endIndex] of Cuboid.#edges) {
const start = corners[startIndex];
const end = corners[endIndex];
const segments = Cuboid.segmentCount(start, end);
for (let i = 0; i < segments; i++)
yield [start.lerp(end, i / segments), start.lerp(end, (i + 1) / segments)];
}
}
matches(min, max) {
return this.min.x === min.x && this.min.y === min.y && this.min.z === min.z
&& this.max.x === max.x && this.max.y === max.y && this.max.z === max.z;
}
edgePoints() {
return Cuboid.edgePoints(this.corners);
}
edgeSegments() {
return Cuboid.edgeSegments(this.corners);
}
}
@@ -0,0 +1,57 @@
import { DebugLine, DebugSphere, debugDrawer } from "@minecraft/debug-utilities";
import { OutlineRenderer } from "./OutlineRenderer.js";
export class DebugOutlineRenderer extends OutlineRenderer {
CORNER_SPHERE_SCALE = 0.1;
#shapes = [];
#isDrawing = false;
start() {
this.stop();
this.#isDrawing = true;
this.#draw();
}
stop() {
this.#isDrawing = false;
for (const shape of this.#shapes)
shape.remove();
this.#shapes.length = 0;
}
setBounds(dimension, min, max) {
const changed = super.setBounds(dimension, min, max);
if (changed && this.#isDrawing)
this.start();
return changed;
}
#draw() {
if (this.drawCorners) {
for (const corner of this.corners) {
const cornerSphereShape = this.#cornerSphere(corner);
this.#add(cornerSphereShape, this.cornerColor);
}
}
if (this.drawEdges) {
let index = 0;
for (const [start, end] of this.cuboid.edgeSegments()) {
const edgeShape = new DebugLine(start, end);
const color = this.edgeColors[index++ % this.edgeColors.length];
this.#add(edgeShape, color);
}
}
}
#add(shape, color) {
shape.color = color;
this.#shapes.push(shape);
debugDrawer.addShape(shape);
}
#cornerSphere(corner) {
const sphere = new DebugSphere({ dimension: this.dimension, x: corner.x, y: corner.y, z: corner.z });
sphere.scale = this.CORNER_SPHERE_SCALE;
return sphere;
}
}
@@ -0,0 +1,39 @@
import { OutlineRenderer } from "./OutlineRenderer";
import { DebugOutlineRenderer } from "./DebugOutlineRenderer";
import { ParticleOutlineRenderer } from "./ParticleOutlineRenderer";
export class HybridOutlineRenderer extends OutlineRenderer {
#edgeRenderer;
#cornerRenderer;
constructor(dimension, min, max, particleTiming) {
super(dimension, min, max);
this.#edgeRenderer = new DebugOutlineRenderer(dimension, min, max);
this.#edgeRenderer.drawCorners = false;
this.#cornerRenderer = new ParticleOutlineRenderer(dimension, min, max, particleTiming);
this.#cornerRenderer.drawEdges = false;
}
start() {
this.#edgeRenderer.start();
this.#cornerRenderer.start();
}
stop() {
this.#edgeRenderer.stop();
this.#cornerRenderer.stop();
}
setBounds(dimension, min, max) {
const changed = super.setBounds(dimension, min, max);
this.#edgeRenderer.setBounds(dimension, min, max);
this.#cornerRenderer.setBounds(dimension, min, max);
return changed;
}
addStandaloneCorners(locations) {
super.addStandaloneCorners(locations);
this.#edgeRenderer.addStandaloneCorners(locations);
this.#cornerRenderer.addStandaloneCorners(locations);
}
}
@@ -0,0 +1,54 @@
import { Vector } from "../../../lib/Vector.js";
import { Cuboid } from "./Cuboid.js";
export class OutlineRenderer {
dimension;
corners;
cuboid;
drawCorners = true;
drawEdges = true;
#standaloneCorners = [];
cornerColor = Object.freeze({ red: 1, green: 1, blue: 1, alpha: 1 });
edgeColors = Object.freeze([
Object.freeze({ red: 0.93333333, green: 0.77647059, blue: 0.13333333, alpha: 1 }),
Object.freeze({ red: 0.09019608, green: 0.09019608, blue: 0.09019608, alpha: 1 })
]);
constructor(dimension, min, max) {
this.#assignBounds(dimension, min, max);
}
setBounds(dimension, min, max) {
if (dimension === this.dimension && this.cuboid.matches(min, max))
return false;
this.#assignBounds(dimension, min, max);
return true;
}
addStandaloneCorners(locations) {
for (const location of locations)
this.#standaloneCorners.push(Vector.from(location));
this.#collectCorners();
}
start() {
throw new Error(`${this.constructor.name} must implement start().`);
}
stop() {
throw new Error(`${this.constructor.name} must implement stop().`);
}
#assignBounds(dimension, min, max) {
this.dimension = dimension;
this.cuboid = new Cuboid(min, max);
this.#collectCorners();
}
#collectCorners() {
this.corners = this.#standaloneCorners.length === 0
? this.cuboid.corners
: [...this.cuboid.corners, ...this.#standaloneCorners];
}
}
@@ -0,0 +1,55 @@
import { MolangVariableMap, system, TicksPerSecond } from "@minecraft/server";
import { OutlineRenderer } from "./OutlineRenderer.js";
const DEFAULT_TIMING = Object.freeze({ drawIntervalTicks: 10, lifetimeTicks: 20 });
export class ParticleOutlineRenderer extends OutlineRenderer {
#outlineParticle = "construct:outline";
#drawIntervalTicks;
#lifetimeSeconds;
#runner;
constructor(dimension, min, max, timing) {
super(dimension, min, max);
const { drawIntervalTicks, lifetimeTicks } = { ...DEFAULT_TIMING, ...timing };
this.#drawIntervalTicks = drawIntervalTicks;
this.#lifetimeSeconds = lifetimeTicks / TicksPerSecond;
}
start() {
this.#runner = system.runInterval(() => this.#draw(), this.#drawIntervalTicks);
}
stop() {
if (!this.#runner)
return;
system.clearRun(this.#runner);
this.#runner = void 0;
}
#draw() {
if (this.drawCorners) {
for (const corner of this.corners)
this.#spawn(corner, this.cornerColor);
}
if (this.drawEdges) {
let index = 0;
for (const point of this.cuboid.edgePoints()) {
const color = this.edgeColors[index++ % this.edgeColors.length];
this.#spawn(point, color);
}
}
}
#spawn(location, color) {
const molang = new MolangVariableMap();
molang.setColorRGBA("dot_color", color);
molang.setFloat("lifetime", this.#lifetimeSeconds);
try {
this.dimension.spawnParticle(this.#outlineParticle, location, molang);
} catch {
/* pass */
}
}
}
@@ -0,0 +1,52 @@
import { StructureNotFoundError } from "../../Errors/StructureNotFoundError";
import { Cuboid } from "./Cuboid";
import { createOutlineRenderer } from "./createOutlineRenderer.js";
export class StructureOutliner {
instance;
#renderer;
constructor(instance) {
this.instance = instance;
}
refresh() {
this.#renderer?.stop();
this.#renderer = void 0;
if (!this.instance.isEnabled())
return;
const view = this.#readView();
if (!view)
return;
this.#renderer = this.#createRenderer(view);
if (view.wholeStructureCorners)
this.#renderer.addStandaloneCorners(view.wholeStructureCorners);
this.#renderer.start();
}
#readView() {
try {
const dimension = this.instance.getDimension();
const bounds = this.instance.getBounds();
const min = this.instance.toGlobalCoords(bounds.min);
const max = this.instance.toGlobalCoords(bounds.max);
if (!this.instance.hasLayerSelected())
return { dimension, min, max };
const layer = this.instance.getLayerBounds(this.instance.getLayer());
return {
dimension,
min: this.instance.toGlobalCoords(layer.min),
max: this.instance.toGlobalCoords(layer.max),
wholeStructureCorners: Cuboid.cornersOf(min, max)
};
} catch (error) {
if (error instanceof StructureNotFoundError)
return void 0;
throw error;
}
}
#createRenderer(view) {
return createOutlineRenderer(this.instance.getRenderMode(), view.dimension, view.min, view.max);
}
}
@@ -0,0 +1,9 @@
import { renderProfileOf } from "../../Enums/RenderMode";
import { HybridOutlineRenderer } from "./HybridOutlineRenderer.js";
import { ParticleOutlineRenderer } from "./ParticleOutlineRenderer.js";
export function createOutlineRenderer(renderMode, dimension, min, max, particleTiming) {
const profile = renderProfileOf(renderMode);
const Renderer = profile.hybridOutline ? HybridOutlineRenderer : ParticleOutlineRenderer;
return new Renderer(dimension, min, max, particleTiming);
}
@@ -0,0 +1,105 @@
import { system } from "@minecraft/server";
import { BlockVerificationLevel } from "../../Enums/BlockVerificationLevel";
import { renderProfileOf } from "../../Enums/RenderMode";
import { RefreshRate } from "../../Verifier/RefreshRate";
import { Vector } from "../../../lib/Vector";
import { DebugBoxLayer } from "./DebugBoxLayer";
import { PreviewParticleLayer } from "./PreviewParticleLayer";
import { PreviewSweep } from "./PreviewSweep";
const RENDER_INTERVAL_TICKS = 1;
export class BlockPreviewRenderer {
instance;
#profile;
#sweep = new PreviewSweep();
#boxes = new DebugBoxLayer();
#particles;
#runner;
constructor(instance) {
this.instance = instance;
this.#applyRenderMode();
}
refresh(isPriority = false) {
this.#stop();
this.#applyRenderMode();
if (!this.instance.isEnabled() || !this.instance.options.verifier.isEnabled)
return;
if (isPriority)
this.#sweep.arm();
this.#runner = system.runInterval(() => this.#renderNextSlice(), RENDER_INTERVAL_TICKS);
}
renderBlockAt(location) {
if (!this.#runner)
return;
const frame = this.#openFrame();
if (frame)
this.#drawCell(frame, location);
}
#applyRenderMode() {
this.#profile = renderProfileOf(this.instance.options.renderMode);
this.#particles = new PreviewParticleLayer(this.#profile.blockPreview);
}
#stop() {
this.#boxes.clear();
if (!this.#runner)
return;
system.clearRun(this.#runner);
this.#runner = void 0;
this.#sweep.reset();
}
#renderNextSlice() {
const frame = this.#openFrame();
if (!frame)
return;
const refreshSeconds = this.instance.options.verifier.refreshSeconds;
for (const location of this.#sweep.locations(frame.bounds, frame.volume, refreshSeconds))
this.#drawCell(frame, location);
}
#openFrame() {
const bounds = this.instance.getActiveBounds();
const volume = Vector.volume(bounds.min, bounds.max);
if (volume <= 0)
return void 0;
const grid = this.instance.verifier.getCompletedGrid();
if (!grid?.matchesBounds(bounds))
return void 0;
this.#boxes.retarget(bounds);
return {
bounds,
volume,
grid,
dimension: this.instance.getDimension(),
lifetimeSeconds: RefreshRate.cycleSeconds(volume, this.instance.options.verifier.refreshSeconds)
};
}
#drawCell(frame, location) {
const verificationLevel = frame.grid.get(location);
const origin = this.instance.toGlobalCoords(location);
if (this.#profile.debugMarkers) {
const index = frame.grid.indexOf(location);
this.#boxes.draw(index, frame.dimension, origin, verificationLevel);
}
if (!this.#profile.particleOverlays)
return;
if (verificationLevel === BlockVerificationLevel.Unknown || verificationLevel === BlockVerificationLevel.Air)
return;
this.#particles.draw({
dimension: frame.dimension,
origin,
block: this.instance.getBlock(location),
verificationLevel,
lifetimeSeconds: frame.lifetimeSeconds,
occlusionMask: frame.grid.occlusionMaskAt(location)
});
}
}
@@ -0,0 +1,73 @@
import { DebugBox, debugDrawer } from "@minecraft/debug-utilities";
import { boxStyleOf } from "../VerificationStyle";
export class DebugBoxLayer {
#boxes = new Map();
#bounds;
retarget(bounds) {
if (this.#matchesBounds(bounds))
return;
this.clear();
this.#bounds = {
min: { x: bounds.min.x, y: bounds.min.y, z: bounds.min.z },
max: { x: bounds.max.x, y: bounds.max.y, z: bounds.max.z }
};
}
draw(index, dimension, origin, verificationLevel) {
if (index === -1)
return;
const style = boxStyleOf(verificationLevel);
const existing = this.#boxes.get(index);
if (!style)
this.#hide(existing);
else if (existing)
this.#restyle(existing, style);
else
this.#boxes.set(index, this.#create(dimension, origin, style));
}
clear() {
for (const entry of this.#boxes.values())
this.#hide(entry);
this.#boxes.clear();
this.#bounds = void 0;
}
#matchesBounds(bounds) {
return this.#bounds !== void 0
&& this.#bounds.min.x === bounds.min.x && this.#bounds.max.x === bounds.max.x
&& this.#bounds.min.y === bounds.min.y && this.#bounds.max.y === bounds.max.y
&& this.#bounds.min.z === bounds.min.z && this.#bounds.max.z === bounds.max.z;
}
#hide(entry) {
if (!entry?.isVisible)
return;
entry.box.remove();
entry.isVisible = false;
}
#restyle(entry, style) {
entry.box.color = style.color;
entry.box.scale = style.scale;
if (entry.isVisible)
return;
debugDrawer.addShape(entry.box);
entry.isVisible = true;
}
#create(dimension, origin, style) {
const box = new DebugBox({
dimension,
x: origin.x + 0.5,
y: origin.y + 0.5,
z: origin.z + 0.5
});
box.color = style.color;
box.scale = style.scale;
debugDrawer.addShape(box);
return { box, isVisible: true };
}
}
@@ -0,0 +1,18 @@
import { DebugBox, debugDrawer } from "@minecraft/debug-utilities";
import { boxStyleOf } from "../VerificationStyle";
export function drawExpiringDebugBox(dimension, origin, verificationLevel, lifetimeSeconds) {
const style = boxStyleOf(verificationLevel);
if (!style)
return;
const box = new DebugBox({
dimension,
x: origin.x + 0.5,
y: origin.y + 0.5,
z: origin.z + 0.5
});
box.color = style.color;
box.scale = style.scale;
box.timeLeft = lifetimeSeconds;
debugDrawer.addShape(box);
}
@@ -0,0 +1,69 @@
import { MolangVariableMap } from "@minecraft/server";
import { DEBUG_CONFIG } from "../../../consts";
const BLOCK_CENTER = 0.5;
const PIXELS_PER_BLOCK = 16;
export class FaceParticleSpawner {
#molang = new MolangVariableMap();
#location = { x: 0, y: 0, z: 0 };
#normal = { x: 0, y: 0, z: 0 };
#size = { x: 0, y: 0, z: 0 };
#uv = { x: 0, y: 0, z: 0 };
#uvSize = { x: 0, y: 0, z: 0 };
#dimension;
#origin;
#color;
startBlock(dimension, origin, lifetimeSeconds) {
this.#dimension = dimension;
this.#origin = origin;
this.#color = void 0;
this.#molang.setFloat("lifetime", lifetimeSeconds);
}
spawnFace(face, color, material, scale) {
this.#setColor(color);
this.#setGeometry(face, scale);
this.#setUv(face);
try {
this.#dimension.spawnParticle(`construct:block_face_${material}`, this.#location, this.#molang);
} catch {
/* pass */
}
}
#setColor(color) {
if (color === this.#color)
return;
this.#molang.setColorRGBA("face_color", color);
this.#color = color;
}
#setGeometry(face, scale) {
this.#location.x = this.#origin.x + BLOCK_CENTER + (face.center[0] / PIXELS_PER_BLOCK - BLOCK_CENTER) * scale;
this.#location.y = this.#origin.y + BLOCK_CENTER + (face.center[1] / PIXELS_PER_BLOCK - BLOCK_CENTER) * scale;
this.#location.z = this.#origin.z + BLOCK_CENTER + (face.center[2] / PIXELS_PER_BLOCK - BLOCK_CENTER) * scale;
this.#normal.x = face.facing[0];
this.#normal.y = face.facing[1];
this.#normal.z = face.facing[2];
this.#size.x = (face.width / PIXELS_PER_BLOCK) * 0.5 * scale;
this.#size.y = (face.height / PIXELS_PER_BLOCK) * 0.5 * scale;
this.#size.z = face.roll;
this.#molang.setVector3("normal", this.#normal);
this.#molang.setVector3("size", this.#size);
}
#setUv(face) {
this.#uv.x = DEBUG_CONFIG.enable ? DEBUG_CONFIG.render_all_textures_as.u : face.uv.x;
this.#uv.y = DEBUG_CONFIG.enable ? DEBUG_CONFIG.render_all_textures_as.v : face.uv.y;
this.#uvSize.x = face.uv.w;
this.#uvSize.y = face.uv.h;
this.#molang.setVector3("uv", this.#uv);
this.#molang.setVector3("uv_size", this.#uvSize);
}
}
@@ -0,0 +1,69 @@
import { BlockVerificationLevel } from "../../Enums/BlockVerificationLevel";
import { CellFlags } from "../../Verifier/CellFlags";
import { CubeFaceLibrary } from "../model/CubeFaceLibrary";
import { blockModelResolver } from "../model/BlockModelResolver";
import { UNRESOLVED_FACE_STYLE, WATER_MATERIAL, particleStyleOf } from "../VerificationStyle";
import { FaceParticleSpawner } from "./FaceParticleSpawner";
export class PreviewParticleLayer {
#showsBlockPreview;
#spawner = new FaceParticleSpawner();
constructor(showsBlockPreview) {
this.#showsBlockPreview = showsBlockPreview;
}
draw(cell) {
const style = particleStyleOf(cell.verificationLevel, this.#showsBlockPreview);
if (!style || !cell.block)
return;
const culls = this.#cullMasks(cell);
this.#spawner.startBlock(cell.dimension, cell.origin, cell.lifetimeSeconds);
for (const layer of this.#faceLayers(cell, style.material))
this.#drawLayer(cell, layer, style, culls);
}
#cullMasks(cell) {
if (this.#isMarkerCube(cell.verificationLevel))
return { opaque: 0, marker: 0 };
const opaque = CellFlags.opaqueMask(cell.occlusionMask);
return { opaque, marker: opaque | CellFlags.markerMask(cell.occlusionMask) };
}
#faceLayers(cell, material) {
if (!this.#showsBlockPreview || cell.verificationLevel !== BlockVerificationLevel.Missing)
return [{ faces: CubeFaceLibrary.createPlainCubeFaces(), material, isMarker: true }];
const layers = [{
faces: blockModelResolver.facesOf(cell.block),
material: blockModelResolver.isWater(cell.block) ? WATER_MATERIAL : material
}];
if (cell.block.isWaterlogged)
layers.push({ faces: blockModelResolver.waterloggedFaces(), material: WATER_MATERIAL });
return layers;
}
#drawLayer(cell, layer, style, culls) {
const layerCull = layer.isMarker ? culls.marker : culls.opaque;
for (const face of layer.faces) {
const unresolved = face.missing === true;
if (this.#isCulled(face, unresolved ? culls.marker : layerCull))
continue;
try {
if (unresolved)
this.#spawner.spawnFace(face, UNRESOLVED_FACE_STYLE.color, UNRESOLVED_FACE_STYLE.material, style.scale);
else
this.#spawner.spawnFace(face, style.color, layer.material, style.scale);
} catch (error) {
console.warn(`Failed to render face for block ${cell.block.typeId} at ${JSON.stringify(cell.origin)} with verification level ${cell.verificationLevel}:`, error, error.stack);
}
}
}
#isCulled(face, cullMask) {
return face.cull !== void 0 && ((cullMask >> face.cull) & 1) === 1;
}
#isMarkerCube(verificationLevel) {
return verificationLevel === BlockVerificationLevel.Missing && !this.#showsBlockPreview;
}
}
@@ -0,0 +1,61 @@
import { BlockBudget } from "../../Verifier/BlockBudget";
import { PriorityPass } from "../../Verifier/PriorityPass";
import { RefreshRate } from "../../Verifier/RefreshRate";
export class PreviewSweep {
#cursor = 0;
#budget = new BlockBudget();
#priorityPass = new PriorityPass();
arm() {
this.#priorityPass.arm();
}
reset() {
this.#cursor = 0;
this.#budget.clear();
}
*locations(bounds, volume, refreshSeconds) {
this.#budget.credit(this.#blocksPerTick(volume, refreshSeconds));
if (this.#budget.isExhausted())
return;
if (this.#cursor >= volume)
this.#cursor = 0;
while (this.#cursor < volume && !this.#budget.isExhausted()) {
yield locationAt(bounds, this.#cursor);
this.#budget.spend(1);
this.#cursor++;
}
if (this.#cursor >= volume) {
this.#cursor = 0;
this.#priorityPass.disarm();
}
}
#blocksPerTick(volume, refreshSeconds) {
if (this.#priorityPass.isArmed())
return RefreshRate.priorityBlocksPerTick();
return RefreshRate.blocksPerTick(volume, refreshSeconds);
}
}
function locationAt(bounds, index) {
const width = bounds.max.x - bounds.min.x;
const depth = bounds.max.z - bounds.min.z;
const layerArea = width * depth;
const y = bounds.min.y + Math.floor(index / layerArea);
const withinLayer = index % layerArea;
if (bounds.max.x < bounds.max.z) {
return {
x: bounds.min.x + (withinLayer % width),
y,
z: bounds.min.z + Math.floor(withinLayer / width)
};
}
return {
x: bounds.min.x + Math.floor(withinLayer / depth),
y,
z: bounds.min.z + (withinLayer % depth)
};
}
+76 -14
View File
@@ -2,9 +2,25 @@ import { world } from "@minecraft/server";
import { Vector } from "../../lib/Vector"; import { Vector } from "../../lib/Vector";
import { StructureNotFoundError } from "../Errors/StructureNotFoundError"; import { StructureNotFoundError } from "../Errors/StructureNotFoundError";
const NOT_LOADED = 0;
const NO_BLOCK = 1;
const SLOT_WIDTHS = [
{ maxSlot: 0xFF, ArrayType: Uint8Array },
{ maxSlot: 0xFFFF, ArrayType: Uint16Array },
{ maxSlot: 0xFFFFFFFF, ArrayType: Uint32Array },
];
export class Structure { export class Structure {
structureId; structureId;
#structure; #structure;
#size;
#paletteSlots;
#widthIndex = 0;
#palette = [void 0, void 0];
#slotsByKey = new Map();
constructor(structureId) { constructor(structureId) {
this.structureId = structureId; this.structureId = structureId;
@@ -12,27 +28,74 @@ export class Structure {
if (!this.#structure) if (!this.#structure)
throw new StructureNotFoundError(structureId); throw new StructureNotFoundError(structureId);
this.#structure.saveToWorld(); this.#structure.saveToWorld();
this.#size = { ...this.#structure.size };
this.#paletteSlots = new SLOT_WIDTHS[0].ArrayType(this.#size.x * this.#size.y * this.#size.z);
} }
getHeight() { getHeight() {
return this.#structure.size.y; return this.#size.y;
} }
getMin() { getMin() {
return new Vector(0, 0, 0); return { x: 0, y: 0, z: 0 };
} }
getMax() { getMax() {
return Vector.from(this.#structure.size); return this.#size;
} }
getBlock(structureLocation) { getBlock(structureLocation) {
const index = this.#toIndex(structureLocation);
if (index === void 0)
return this.#palette[this.#loadSlot(structureLocation)];
const slot = this.#paletteSlots[index];
if (slot !== NOT_LOADED)
return this.#palette[slot];
const loadedSlot = this.#loadSlot(structureLocation);
this.#paletteSlots[index] = loadedSlot;
return this.#palette[loadedSlot];
}
#toIndex(structureLocation) {
const x = Math.floor(structureLocation.x);
const y = Math.floor(structureLocation.y);
const z = Math.floor(structureLocation.z);
if (x < 0 || y < 0 || z < 0 || x >= this.#size.x || y >= this.#size.y || z >= this.#size.z)
return void 0;
return (y * this.#size.z + z) * this.#size.x + x;
}
#loadSlot(structureLocation) {
const blockPermutation = this.#structure.getBlockPermutation(structureLocation); const blockPermutation = this.#structure.getBlockPermutation(structureLocation);
if (!blockPermutation) if (!blockPermutation)
return void 0; return NO_BLOCK;
blockPermutation.location = structureLocation; return this.#intern(blockPermutation, this.#structure.getIsWaterlogged(structureLocation));
blockPermutation.isWaterlogged = this.#structure.getIsWaterlogged(blockPermutation.location); }
return blockPermutation;
#intern(blockPermutation, isWaterlogged) {
const typeId = blockPermutation.type.id;
const states = blockPermutation.getAllStates();
const key = `${typeId}|${isWaterlogged ? 1 : 0}|${JSON.stringify(states)}`;
const interned = this.#slotsByKey.get(key);
if (interned !== void 0)
return interned;
const slot = this.#palette.length;
this.#palette.push({
permutation: blockPermutation,
typeId,
states,
hasStates: Object.keys(states).length > 0,
isWaterlogged
});
this.#slotsByKey.set(key, slot);
if (slot > SLOT_WIDTHS[this.#widthIndex].maxSlot)
this.#widenPaletteSlots();
return slot;
}
#widenPaletteSlots() {
this.#widthIndex++;
this.#paletteSlots = new SLOT_WIDTHS[this.#widthIndex].ArrayType(this.#paletteSlots);
} }
*getBlocks(locations) { *getBlocks(locations) {
@@ -42,23 +105,23 @@ export class Structure {
} }
*getLayerBlocks(layer) { *getLayerBlocks(layer) {
for (let x = 0; x < this.#structure.size.x; x++) { for (let x = 0; x < this.#size.x; x++) {
for (let z = 0; z < this.#structure.size.z; z++) { for (let z = 0; z < this.#size.z; z++) {
yield this.getBlock({ x, y: layer, z }); yield this.getBlock({ x, y: layer, z });
} }
} }
} }
*getAllBlocks() { *getAllBlocks() {
for (let y = 0; y < this.#structure.size.y; y++) { for (let y = 0; y < this.#size.y; y++) {
yield * this.getLayerBlocks(y); yield * this.getLayerBlocks(y);
} }
} }
getLayerLocations(layer) { getLayerLocations(layer) {
const locations = new Set(); const locations = new Set();
for (let x = 0; x < this.#structure.size.x; x++) { for (let x = 0; x < this.#size.x; x++) {
for (let z = 0; z < this.#structure.size.z; z++) { for (let z = 0; z < this.#size.z; z++) {
locations.add(new Vector(x, layer, z)); locations.add(new Vector(x, layer, z));
} }
} }
@@ -67,7 +130,7 @@ export class Structure {
getAllLocations() { getAllLocations() {
const locations = new Set(); const locations = new Set();
for (let y = 0; y < this.#structure.size.y; y++) { for (let y = 0; y < this.#size.y; y++) {
const layerLocations = this.getLayerLocations(y); const layerLocations = this.getLayerLocations(y);
for (const location of layerLocations) { for (const location of layerLocations) {
locations.add(location); locations.add(location);
@@ -76,4 +139,3 @@ export class Structure {
return locations; return locations;
} }
} }
@@ -0,0 +1,40 @@
import { BlockPermutation } from "@minecraft/server";
import { bannedToValidBlockMap, resetToBlockStates, blockIdToItemStackMap } from "../../options/easyPlaceConversions";
export class StructureBlockConverter {
static sanitizeToPermutation(structureBlock) {
const permutation = StructureBlockConverter.fromBannedBlockToValidPermutation(structureBlock);
return StructureBlockConverter.toDefaultState(permutation);
}
static sanitizeToPlaceableItemStack(structureBlock) {
const permutation = StructureBlockConverter.sanitizeToPermutation(structureBlock);
return StructureBlockConverter.toPlaceableItemStack(permutation);
}
static fromBannedBlockToValidPermutation(structureBlock) {
const blockId = structureBlock.typeId.replace('minecraft:', '');
if (Object.keys(bannedToValidBlockMap).includes(blockId))
return BlockPermutation.resolve(bannedToValidBlockMap[blockId], structureBlock.states);
if (blockId === "bubble_column" && structureBlock.isWaterlogged)
return BlockPermutation.resolve('minecraft:water');
return structureBlock.permutation;
}
static toDefaultState(permutation) {
const newStates = {};
for (const [stateKey, stateValue] of Object.entries(permutation.getAllStates())) {
if (resetToBlockStates[stateKey] !== void 0 && stateValue !== resetToBlockStates[stateKey])
newStates[stateKey] = resetToBlockStates[stateKey];
else
newStates[stateKey] = stateValue;
}
return BlockPermutation.resolve(permutation.type.id, newStates);
}
static toPlaceableItemStack(permutation) {
const blockId = permutation.type.id.replace('minecraft:', '');
const newItemId = blockIdToItemStackMap[blockId];
return newItemId ? new ItemStack(newItemId) : permutation.getItemStack();
}
}
@@ -0,0 +1,74 @@
import { EntityComponentTypes, EquipmentSlot, GameMode } from "@minecraft/server";
import { fetchMatchingItemSlotIndex } from "../../utils";
import { Raycaster } from "../Raycaster";
import { StructureBlockConverter } from "./StructureBlockConverter";
export class StructurePickBlock {
static #HOTBAR_ITEM_SLOTS = 9;
static perform(player) {
const placeableItemStack = StructurePickBlock.#getItemStackFromTargetedStructure(player);
if (!placeableItemStack)
return;
const itemSlotIndexToUse = fetchMatchingItemSlotIndex(player, placeableItemStack?.typeId);
const equippableComponent = player.getComponent(EntityComponentTypes.Equippable);
const mainHandSlot = equippableComponent?.getEquipmentSlot(EquipmentSlot.Mainhand);
if (itemSlotIndexToUse)
StructurePickBlock.#swapItemIntoHotbar(player, itemSlotIndexToUse, mainHandSlot);
else if (player.getGameMode() === GameMode.Creative)
StructurePickBlock.#pickBlockCreative(player, mainHandSlot, placeableItemStack);
}
static #getItemStackFromTargetedStructure(player) {
const hit = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: true, useActiveLayer: true });
if (!hit?.block)
return void 0;
return StructureBlockConverter.sanitizeToPlaceableItemStack(hit?.block);
}
static #swapItemIntoHotbar(player, sourceSlotIndex, hotbarReplacementSlot) {
const inventoryContainer = player.getComponent(EntityComponentTypes.Inventory)?.container;
if (sourceSlotIndex < StructurePickBlock.#HOTBAR_ITEM_SLOTS) {
player.selectedSlotIndex = sourceSlotIndex;
return;
}
const emptyHotbarSlotIndex = StructurePickBlock.#getFirstEmptyHotbarSlotIndex(inventoryContainer);
const sourceSlot = inventoryContainer.getSlot(sourceSlotIndex);
if (emptyHotbarSlotIndex !== void 0) {
const itemToMove = sourceSlot.getItem();
sourceSlot.setItem(void 0);
const emptyHotbarSlot = inventoryContainer.getSlot(emptyHotbarSlotIndex);
emptyHotbarSlot.setItem(itemToMove);
player.selectedSlotIndex = emptyHotbarSlotIndex;
} else {
StructurePickBlock.#swapSlots(sourceSlot, hotbarReplacementSlot)
}
}
static #pickBlockCreative(player, mainHandSlot, placeableItemStack) {
const inventoryContainer = player.getComponent(EntityComponentTypes.Inventory)?.container;
const firstEmptySlotNum = inventoryContainer.firstEmptySlot();
if (firstEmptySlotNum === void 0) {
player.onScreenDisplay.setActionBar({ translate: 'construct.pickblock.inventoryfull' });
return;
}
const emptySlot = inventoryContainer?.getSlot(firstEmptySlotNum);
emptySlot.setItem(placeableItemStack);
StructurePickBlock.#swapItemIntoHotbar(player, firstEmptySlotNum, mainHandSlot);
}
static #getFirstEmptyHotbarSlotIndex(inventoryContainer) {
for (let index = 0; index < StructurePickBlock.#HOTBAR_ITEM_SLOTS; index++) {
if (!inventoryContainer.getItem(index))
return index;
}
return void 0;
}
static #swapSlots(slotOne, slotTwo) {
const slotOneItem = slotOne.getItem();
const slotTwoItem = slotTwo.getItem();
slotOne.setItem(slotTwoItem);
slotTwo.setItem(slotOneItem);
}
}
@@ -0,0 +1,19 @@
export class BlockBudget {
#remaining = 0;
credit(blocks) {
this.#remaining += blocks;
}
spend(blocks) {
this.#remaining -= blocks;
}
isExhausted() {
return this.#remaining <= 0;
}
clear() {
this.#remaining = 0;
}
}
@@ -0,0 +1,25 @@
export class CellFlags {
static #sideMask = 0b111111;
static #opaqueShift = 6;
static NONE = 0;
static pack(markerMask, opaqueMask) {
return (markerMask & CellFlags.#sideMask) | ((opaqueMask & CellFlags.#sideMask) << CellFlags.#opaqueShift);
}
static markerMask(packed) {
return packed & CellFlags.#sideMask;
}
static opaqueMask(packed) {
return (packed >> CellFlags.#opaqueShift) & CellFlags.#sideMask;
}
static hasMarker(packed, side) {
return ((packed >> side) & 1) === 1;
}
static hasOpaque(packed, side) {
return ((packed >> (side + CellFlags.#opaqueShift)) & 1) === 1;
}
}
@@ -0,0 +1,34 @@
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel";
import { blockModelResolver } from "../Render/model/BlockModelResolver";
import { CellFlags } from "./CellFlags";
export class CellShape {
#instance;
#showsBlockPreview;
constructor(instance, showsBlockPreview) {
this.#instance = instance;
this.#showsBlockPreview = showsBlockPreview;
}
flagsFor(location, verificationLevel) {
switch (verificationLevel) {
case BlockVerificationLevel.NoMatch:
case BlockVerificationLevel.TypeMatch:
return blockModelResolver.overlaySideMasks();
case BlockVerificationLevel.Match:
return this.#structureBlockFlags(location);
case BlockVerificationLevel.Missing:
return this.#showsBlockPreview ? this.#structureBlockFlags(location) : CellFlags.NONE;
default:
return CellFlags.NONE;
}
}
#structureBlockFlags(location) {
const structureBlock = this.#instance.getBlock(location);
if (structureBlock === void 0)
return CellFlags.NONE;
return blockModelResolver.sideMasksOf(structureBlock);
}
}
@@ -0,0 +1,46 @@
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel";
import { CellShape } from "./CellShape";
export class CellVerifier {
#instance;
#shape;
constructor(instance, showsBlockPreview) {
this.#instance = instance;
this.#shape = new CellShape(instance, showsBlockPreview);
}
verify(location) {
const verificationLevel = this.#levelAt(location);
return {
verificationLevel,
flags: this.#shape.flagsFor(location, verificationLevel)
};
}
#levelAt(location) {
const globalLocation = this.#instance.toGlobalCoords(location);
const worldBlock = this.#instance.getDimension()?.getBlock(globalLocation);
if (!worldBlock)
return BlockVerificationLevel.Skipped;
return this.#compare(worldBlock, this.#instance.getBlock(location));
}
#compare(worldBlock, structureBlock) {
if (!structureBlock)
return BlockVerificationLevel.Air;
if (structureBlock.typeId === "minecraft:air")
return worldBlock.isAir ? BlockVerificationLevel.Air : BlockVerificationLevel.NoMatch;
if (worldBlock.isAir)
return BlockVerificationLevel.Missing;
if (worldBlock.typeId !== structureBlock.typeId)
return BlockVerificationLevel.NoMatch;
if (worldBlock.isWaterlogged !== structureBlock.isWaterlogged)
return BlockVerificationLevel.TypeMatch;
if (!structureBlock.hasStates)
return BlockVerificationLevel.Match;
if (worldBlock.permutation.matches(structureBlock.typeId, structureBlock.states))
return BlockVerificationLevel.Match;
return BlockVerificationLevel.TypeMatch;
}
}
@@ -0,0 +1,22 @@
import { Vector } from "../../lib/Vector";
import { Side } from "../Enums/Side";
import { instanceCollection } from "../Instance/InstanceCollection";
export class ChangedCellPatcher {
#patchedOffsets = Object.freeze([Vector.zero, ...Side.OFFSETS]);
patchAround(dimensionId, worldLocation) {
for (const offset of this.#patchedOffsets)
this.#patchCell(dimensionId, Vector.add(worldLocation, offset));
}
#patchCell(dimensionId, worldLocation) {
for (const instance of instanceCollection.getInstancesAt(dimensionId, worldLocation)) {
if (!instance.verifier || !instance.previewRenderer)
continue;
const location = instance.toStructureCoords(worldLocation);
instance.verifier.patchCell(location);
instance.previewRenderer.renderBlockAt(location);
}
}
}
@@ -0,0 +1,22 @@
export class ChunkGrid {
#chunkSize = 16;
#chunkMask = this.#chunkSize - 1;
#chunkShift = 4;
#chunkKeyStride = 4194304;
#origin;
constructor(origin) {
this.#origin = origin;
}
keyAt(x, z) {
const chunkX = (x + this.#origin.x) >> this.#chunkShift;
const chunkZ = (z + this.#origin.z) >> this.#chunkShift;
return chunkX * this.#chunkKeyStride + chunkZ;
}
endOfSpanX(x) {
return x + this.#chunkSize - ((x + this.#origin.x) & this.#chunkMask);
}
}
@@ -0,0 +1,32 @@
import { VerificationGrid } from "./VerificationGrid";
export class GridBuffers {
#completed;
#filling;
completed() {
return this.#completed;
}
filling() {
return this.#filling;
}
beginPass(bounds) {
this.#filling = this.#recycled(this.#filling, bounds);
return this.#filling;
}
commit() {
const finished = this.#filling;
this.#filling = this.#completed;
this.#completed = finished;
}
#recycled(grid, bounds) {
if (!grid?.matchesBounds(bounds))
return new VerificationGrid(bounds);
grid.clear();
return grid;
}
}
@@ -0,0 +1,19 @@
// Armed means the drawn picture does not yet describe the current bounds, so
// the next pass should run flat out. Reading and clearing are separate because
// each holder clears at a different point: the verifier only when a pass
// commits, the render cursor only when a lap ends.
export class PriorityPass {
#isArmed = false;
arm() {
this.#isArmed = true;
}
isArmed() {
return this.#isArmed;
}
disarm() {
this.#isArmed = false;
}
}
@@ -0,0 +1,32 @@
import { TicksPerSecond } from "@minecraft/server";
export class RefreshRate {
static MAX_BLOCKS_PER_TICK = 30;
static MIN_SECONDS = 1;
static MAX_SECONDS = 60;
static SECONDS_STEP = 1;
static DEFAULT_SECONDS = 15;
static clampSeconds(seconds) {
return Math.min(RefreshRate.MAX_SECONDS, Math.max(RefreshRate.MIN_SECONDS, seconds));
}
static priorityBlocksPerTick() {
return RefreshRate.MAX_BLOCKS_PER_TICK;
}
static blocksPerTick(volume, refreshSeconds) {
if (volume <= 0)
return 0;
const seconds = Math.max(refreshSeconds, RefreshRate.MIN_SECONDS);
return Math.min(volume / (seconds * TicksPerSecond), RefreshRate.MAX_BLOCKS_PER_TICK);
}
static cycleSeconds(volume, refreshSeconds) {
const rate = RefreshRate.blocksPerTick(volume, refreshSeconds);
if (rate <= 0)
return 0;
return volume / (rate * TicksPerSecond);
}
}
@@ -0,0 +1,24 @@
export class SkippedChunkTracker {
#unloadedChunks = new Set();
#outOfBoundsChunks = new Set();
startLayer() {
this.#outOfBoundsChunks.clear();
}
isSkipped(chunkKey) {
return this.#unloadedChunks.has(chunkKey) || this.#outOfBoundsChunks.has(chunkKey);
}
trackError(error, chunkKey) {
if (error?.name === 'LocationInUnloadedChunkError') {
this.#unloadedChunks.add(chunkKey);
return true;
}
if (error?.name === 'LocationOutOfWorldBoundariesError') {
this.#outOfBoundsChunks.add(chunkKey);
return true;
}
return false;
}
}
@@ -1,144 +1,147 @@
import { BlockVerifier } from "./BlockVerifier"; import { system } from "@minecraft/server";
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel";
import { BlockVerificationLevelRender } from "../Render/BlockVerificationLevelRender";
import { system, TicksPerSecond } from "@minecraft/server";
import { Vector } from "../../lib/Vector"; import { Vector } from "../../lib/Vector";
import { CellVerifier } from "./CellVerifier";
const MIN_TRACK_PLAYER_DISTANCE = 0; import { GridBuffers } from "./GridBuffers";
const MAX_TRACK_PLAYER_DISTANCE = 7; import { PriorityPass } from "./PriorityPass";
const MIN_LIFETIME = 8; import { RefreshRate } from "./RefreshRate";
import { DebugBoxSweepObserver, SilentSweepObserver } from "./SweepObserver";
import { VerificationRun } from "./VerificationRun";
import { VerificationSweep } from "./VerificationSweep";
import { InstanceVerifierSettings, StandaloneVerifierSettings } from "./VerifierSettings";
export class StructureVerifier { export class StructureVerifier {
instance; #instance;
particleLifetime; #settings;
#buffers = new GridBuffers();
#priorityPass = new PriorityPass();
#run;
#loop;
#isPassPending = false;
locationsToVerify; static forInstance(instance) {
blockVerificationLevels; return new StructureVerifier(instance, new InstanceVerifierSettings(instance));
isLocationPopulationComplete;
isVerificationComplete;
shouldStartNextVerification;
lastCompleteVerificationLevels;
#runner;
#verifyJob;
#populateJob = {};
constructor(instance, { isEnabled = false, trackPlayerDistance = 0, particleLifetime = 10, isStandalone = false } = {}) {
this.instance = instance;
this.particleLifetime = Math.max(particleLifetime, MIN_LIFETIME);
if (isStandalone) {
this.isStandalone = isStandalone;
this.enabled = isEnabled;
this.trackPlayerDistance = trackPlayerDistance;
} else {
this.instance.options.setVerifierEnabled(isEnabled);
this.instance.options.setVerifierDistance(trackPlayerDistance);
}
this.locationsToVerify = new Set();
} }
startContinuousVerification() { static standalone(instance, options) {
this.shouldStartNextVerification = true; return new StructureVerifier(instance, new StandaloneVerifierSettings(options));
this.#runner = system.runInterval(() => {
if (this.shouldStartNextVerification)
this.verifyStructure();
});
} }
stopContinuousVerification() { constructor(instance, settings) {
if (!this.#runner) this.#instance = instance;
return; this.#settings = settings;
system.clearRun(this.#runner);
this.#runner = void 0;
}
refresh() {
this.stopContinuousVerification();
if (!this.instance.isEnabled())
return;
this.startContinuousVerification();
} }
isEnabled() { isEnabled() {
if (this.isStandalone) return this.#settings.isEnabled();
return this.enabled;
return this.instance.options.verifier.isEnabled;
} }
getTrackPlayerDistance() { getCompletedGrid() {
let distance; return this.#buffers.completed();
if (this.isStandalone) }
distance = this.trackPlayerDistance;
else refresh(isPriority = false) {
distance = this.instance.options.verifier.trackPlayerDistance this.#stopLoop();
return Math.min(MAX_TRACK_PLAYER_DISTANCE, Math.max(MIN_TRACK_PLAYER_DISTANCE, distance)); this.#cancelRun();
if (isPriority)
this.#priorityPass.arm();
if (this.#instance.isEnabled())
this.#startLoop();
} }
async verifyStructure(shouldRender = false) { async verifyStructure(shouldRender = false) {
if (!this.isEnabled()) if (!this.isEnabled())
return; return void 0;
this.initVerification(); const bounds = this.#instance.getActiveBounds();
return new Promise(async (resolve) => { const volume = Vector.volume(bounds.min, bounds.max);
if (this.#verifyJob) if (volume <= 0)
system.clearJob(this.#verifyJob); return this.#buffers.completed();
this.#verifyJob = system.runJob(this.verifyBlocks(shouldRender)); this.#cancelRun();
const checker = system.runInterval(() => { const run = this.#beginRun(bounds, volume, shouldRender);
if (this.isVerificationComplete) { this.#finishRun(run, await run.start());
system.clearRun(checker); return this.#buffers.completed();
this.lastCompleteVerificationLevels = JSON.parse(JSON.stringify(this.blockVerificationLevels));
this.shouldStartNextVerification = true;
resolve(this.blockVerificationLevels);
} }
}, 1);
patchCell(location) {
const completed = this.#buffers.completed();
if (!completed)
return;
const cell = this.#tryVerifyCell(location);
if (!cell)
return;
completed.setCell(location, cell.verificationLevel, cell.flags);
const filling = this.#buffers.filling()
filling?.setCell(location, cell.verificationLevel, cell.flags);
}
#tryVerifyCell(location) {
try {
return this.#newCellVerifier().verify(location);
} catch {
return void 0;
}
}
#startLoop() {
this.#isPassPending = true;
this.#loop = system.runInterval(() => {
if (this.#isPassPending)
this.verifyStructure();
}); });
} }
initVerification() { #stopLoop() {
this.shouldStartNextVerification = false; if (!this.#loop)
this.locationsToVerify.clear(); return;
this.blockVerificationLevels = { correctlyAir: 0 }; system.clearRun(this.#loop);
this.isLocationPopulationComplete = false; this.#loop = void 0;
this.isVerificationComplete = false;
} }
*verifyBlocks(shouldRender) { #beginRun(bounds, volume, shouldRender) {
const bounds = this.instance.getActiveBounds(); this.#isPassPending = false;
for (let y = bounds.min.y; y < bounds.max.y; y++) { this.#run = new VerificationRun(
for (let z = bounds.min.z; z < bounds.max.z; z++) { this.#newSweep(bounds, shouldRender),
for (let x = bounds.min.x; x < bounds.max.x; x++) { this.#blocksPerTick(volume)
const location = new Vector(x, y, z); );
this.verifyBlock(location, shouldRender); return this.#run;
yield void 0;
}
}
}
this.isVerificationComplete = true;
} }
verifyBlock(location, shouldRender) { #blocksPerTick(volume) {
const verificationLevel = this.getVerificationLevel(location); if (this.#priorityPass.isArmed())
if (verificationLevel === BlockVerificationLevel.Air) { return RefreshRate.priorityBlocksPerTick();
this.blockVerificationLevels.correctlyAir++; return this.#settings.blocksPerTick(volume);
} else {
this.blockVerificationLevels[JSON.stringify(location)] = verificationLevel;
if (shouldRender) {
const dimensionLocation = { dimension: this.instance.getDimension(), location: this.instance.toGlobalCoords(location) };
new BlockVerificationLevelRender(dimensionLocation, verificationLevel, this.particleLifetime/TicksPerSecond);
}
}
} }
getVerificationLevel(location) { #finishRun(run, didComplete) {
const worldBlock = this.instance.getDimension()?.getBlock(this.instance.toGlobalCoords(location)); if (this.#run === run)
if (!worldBlock) this.#run = void 0;
return BlockVerificationLevel.Skipped; if (!didComplete)
const blockVerifier = new BlockVerifier(worldBlock, this.instance); return;
return blockVerifier.verify(); this.#priorityPass.disarm();
this.#buffers.commit();
this.#isPassPending = true;
} }
getLastVerificationLevels() { #cancelRun() {
if (!this.lastCompleteVerificationLevels) this.#run?.cancel();
return {}; }
return this.lastCompleteVerificationLevels;
#newSweep(bounds, shouldRender) {
return new VerificationSweep({
bounds,
grid: this.#buffers.beginPass(bounds),
origin: this.#instance.toGlobalCoords({ x: 0, y: 0, z: 0 }),
cellVerifier: this.#newCellVerifier(),
observer: this.#newObserver(shouldRender)
});
}
#newCellVerifier() {
return new CellVerifier(this.#instance, this.#settings.showsBlockPreview());
}
#newObserver(shouldRender) {
if (!shouldRender)
return new SilentSweepObserver();
return new DebugBoxSweepObserver(this.#instance, this.#settings.particleLifetimeSeconds());
} }
} }
@@ -0,0 +1,27 @@
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel";
import { drawExpiringDebugBox } from "../Render/preview/ExpiringDebugBox";
export class SilentSweepObserver {
onCellVerified() {}
}
export class DebugBoxSweepObserver {
#instance;
#lifetimeSeconds;
constructor(instance, lifetimeSeconds) {
this.#instance = instance;
this.#lifetimeSeconds = lifetimeSeconds;
}
onCellVerified(location, verificationLevel) {
if (verificationLevel === BlockVerificationLevel.Air)
return;
drawExpiringDebugBox(
this.#instance.getDimension(),
this.#instance.toGlobalCoords(location),
verificationLevel,
this.#lifetimeSeconds
);
}
}
@@ -0,0 +1,102 @@
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel.js";
import { Side } from "../Enums/Side.js";
import { CellFlags } from "./CellFlags.js";
const LEVEL_COUNT = Object.keys(BlockVerificationLevel).length;
export class VerificationGrid {
#min;
#sizeX;
#sizeY;
#sizeZ;
#levels;
#flags;
constructor(bounds) {
this.#min = { x: bounds.min.x, y: bounds.min.y, z: bounds.min.z };
this.#sizeX = Math.max(bounds.max.x - bounds.min.x, 0);
this.#sizeY = Math.max(bounds.max.y - bounds.min.y, 0);
this.#sizeZ = Math.max(bounds.max.z - bounds.min.z, 0);
this.#levels = new Uint8Array(this.#sizeX * this.#sizeY * this.#sizeZ);
this.#flags = new Uint16Array(this.#levels.length);
}
matchesBounds(bounds) {
return this.#min.x === bounds.min.x && this.#min.y === bounds.min.y && this.#min.z === bounds.min.z
&& this.#sizeX === bounds.max.x - bounds.min.x
&& this.#sizeY === bounds.max.y - bounds.min.y
&& this.#sizeZ === bounds.max.z - bounds.min.z;
}
clear() {
this.#levels.fill(BlockVerificationLevel.Unknown);
this.#flags.fill(CellFlags.NONE);
}
setCell(location, verificationLevel, flags) {
const index = this.indexOf(location);
if (index === -1)
return;
this.#levels[index] = verificationLevel;
this.#flags[index] = flags;
}
setLevel(location, verificationLevel) {
const index = this.indexOf(location);
if (index === -1)
return;
this.#levels[index] = verificationLevel;
}
get(location) {
const index = this.indexOf(location);
if (index === -1)
return BlockVerificationLevel.Unknown;
return this.#levels[index];
}
occlusionMaskAt(location) {
let markerMask = CellFlags.NONE;
let opaqueMask = CellFlags.NONE;
for (let side = 0; side < Side.COUNT; side++) {
const neighborFlags = this.#neighborFlags(location, side);
const facingSide = Side.opposite(side);
if (CellFlags.hasMarker(neighborFlags, facingSide))
markerMask |= 1 << side;
if (CellFlags.hasOpaque(neighborFlags, facingSide))
opaqueMask |= 1 << side;
}
return CellFlags.pack(markerMask, opaqueMask);
}
#neighborFlags(location, side) {
const offset = Side.OFFSETS[side];
const index = this.#indexOfCoords(
location.x + offset.x, location.y + offset.y, location.z + offset.z
);
if (index === -1)
return CellFlags.NONE;
return this.#flags[index];
}
countByLevel() {
const counts = new Uint32Array(LEVEL_COUNT);
for (let index = 0; index < this.#levels.length; index++)
counts[this.#levels[index]]++;
return counts;
}
indexOf(location) {
return this.#indexOfCoords(location.x, location.y, location.z);
}
#indexOfCoords(x, y, z) {
const localX = x - this.#min.x;
const localY = y - this.#min.y;
const localZ = z - this.#min.z;
if (localX < 0 || localY < 0 || localZ < 0
|| localX >= this.#sizeX || localY >= this.#sizeY || localZ >= this.#sizeZ)
return -1;
return (localY * this.#sizeZ + localZ) * this.#sizeX + localX;
}
}
@@ -0,0 +1,67 @@
import { system } from "@minecraft/server";
import { BlockBudget } from "./BlockBudget";
export class VerificationRun {
#sweep;
#blocksPerTick;
#budget = new BlockBudget();
#steps;
#runner;
#settle;
#completion;
constructor(sweep, blocksPerTick) {
this.#sweep = sweep;
this.#blocksPerTick = blocksPerTick;
this.#completion = new Promise((resolve) => { this.#settle = resolve; });
}
start() {
this.#steps = this.#sweep.run();
this.#runner = system.runInterval(() => this.#advance());
return this.#completion;
}
cancel() {
this.#settleWith(false);
}
#advance() {
this.#budget.credit(this.#blocksPerTick);
try {
while (!this.#budget.isExhausted()) {
if (!this.#advanceOneStep())
return;
}
} catch (error) {
this.#settleWith(false);
throw error;
}
}
#advanceOneStep() {
const step = this.#steps.next();
if (step.done) {
this.#settleWith(true);
return false;
}
this.#budget.spend(step.value);
return true;
}
#settleWith(didComplete) {
if (!this.#settle)
return;
const settle = this.#settle;
this.#settle = void 0;
this.#stop();
settle(didComplete);
}
#stop() {
if (this.#runner !== void 0)
system.clearRun(this.#runner);
this.#runner = void 0;
this.#steps = void 0;
}
}
@@ -0,0 +1,71 @@
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel";
import { Vector } from "../../lib/Vector";
import { ChunkGrid } from "./ChunkGrid";
import { SkippedChunkTracker } from "./SkippedChunkTracker";
export class VerificationSweep {
#bounds;
#grid;
#cellVerifier;
#observer;
#chunks;
#skippedChunks = new SkippedChunkTracker();
#location = new Vector();
constructor({ bounds, grid, cellVerifier, observer, origin }) {
this.#bounds = bounds;
this.#grid = grid;
this.#cellVerifier = cellVerifier;
this.#observer = observer;
this.#chunks = new ChunkGrid(origin);
}
*run() {
for (let y = this.#bounds.min.y; y < this.#bounds.max.y; y++) {
this.#skippedChunks.startLayer();
for (let z = this.#bounds.min.z; z < this.#bounds.max.z; z++)
yield* this.#sweepRow(y, z);
}
}
*#sweepRow(y, z) {
for (let x = this.#bounds.min.x; x < this.#bounds.max.x;) {
const endX = this.#sweepChunkSpan(x, y, z);
yield endX - x;
x = endX;
}
}
#sweepChunkSpan(startX, y, z) {
const chunkKey = this.#chunks.keyAt(startX, z);
const endX = Math.min(this.#chunks.endOfSpanX(startX), this.#bounds.max.x);
if (this.#skippedChunks.isSkipped(chunkKey))
this.#markSkipped(startX, endX, y, z);
else
this.#verifySpan(chunkKey, startX, endX, y, z);
return endX;
}
#verifySpan(chunkKey, startX, endX, y, z) {
let x = startX;
try {
for (; x < endX; x++)
this.#verifyCell(this.#location.set(x, y, z));
} catch (error) {
if (!this.#skippedChunks.trackError(error, chunkKey))
throw error;
this.#markSkipped(x, endX, y, z);
}
}
#verifyCell(location) {
const { verificationLevel, flags } = this.#cellVerifier.verify(location);
this.#grid.setCell(location, verificationLevel, flags);
this.#observer.onCellVerified(location, verificationLevel);
}
#markSkipped(startX, endX, y, z) {
for (let x = startX; x < endX; x++)
this.#grid.setLevel(this.#location.set(x, y, z), BlockVerificationLevel.Skipped);
}
}
@@ -0,0 +1,68 @@
import { TicksPerSecond } from "@minecraft/server";
import { renderProfileOf } from "../Enums/RenderMode";
import { RefreshRate } from "./RefreshRate";
const DEFAULT_PARTICLE_LIFETIME_TICKS = 10;
const DEFAULT_BLOCKS_PER_TICK = 10;
class ParticleLifetime {
static MIN_TICKS = 8;
static toSeconds(ticks) {
return Math.max(ticks, ParticleLifetime.MIN_TICKS) / TicksPerSecond;
}
}
export class InstanceVerifierSettings {
#instance;
constructor(instance) {
this.#instance = instance;
}
isEnabled() {
return this.#verifierOptions().isEnabled;
}
blocksPerTick(volume) {
return RefreshRate.blocksPerTick(volume, this.#verifierOptions().refreshSeconds);
}
showsBlockPreview() {
return renderProfileOf(this.#instance.options.renderMode).blockPreview;
}
particleLifetimeSeconds() {
return ParticleLifetime.toSeconds(this.#verifierOptions().particleLifetime);
}
#verifierOptions() {
return this.#instance.options.verifier;
}
}
export class StandaloneVerifierSettings {
#blocksPerTick;
#particleLifetimeTicks;
constructor({ blocksPerTick = DEFAULT_BLOCKS_PER_TICK, particleLifetime = DEFAULT_PARTICLE_LIFETIME_TICKS } = {}) {
this.#blocksPerTick = blocksPerTick;
this.#particleLifetimeTicks = particleLifetime;
}
isEnabled() {
return true;
}
blocksPerTick() {
return this.#blocksPerTick;
}
showsBlockPreview() {
return true;
}
particleLifetimeSeconds() {
return ParticleLifetime.toSeconds(this.#particleLifetimeTicks);
}
}
+4 -2
View File
@@ -1,8 +1,9 @@
import { Command } from '../classes/Commands/Command'; import { Command } from '../classes/Commands/Command';
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server';
import { structureCollection } from '../classes/Structure/StructureCollection'; import { instanceCollection } from '../classes/Instance/InstanceCollection';
import { InstanceExistsError } from '../classes/Errors/InstanceExistsError'; import { InstanceExistsError } from '../classes/Errors/InstanceExistsError';
import { StructureNotFoundError } from '../classes/Errors/StructureNotFoundError'; import { StructureNotFoundError } from '../classes/Errors/StructureNotFoundError';
import { getDefaultRenderMode } from '../options/defaultRenderMode';
export class CreateCommand extends Command { export class CreateCommand extends Command {
constructor() { constructor() {
@@ -34,7 +35,8 @@ export class CreateCommand extends Command {
} }
addStructure(origin, instanceName, structureId) { addStructure(origin, instanceName, structureId) {
structureCollection.add(instanceName, structureId); const playerId = origin.getType() === 'Player' ? origin.getSource().id : void 0;
instanceCollection.add(instanceName, structureId, { renderMode: getDefaultRenderMode(playerId) });
origin.sendMessage({ translate: 'construct.commands.create.success', with: [instanceName, structureId] }); origin.sendMessage({ translate: 'construct.commands.create.success', with: [instanceName, structureId] });
} }
+3 -3
View File
@@ -1,6 +1,6 @@
import { Command } from '../classes/Commands/Command'; import { Command } from '../classes/Commands/Command';
import { CustomCommandParamType, CustomCommandStatus, CommandPermissionLevel, system } from '@minecraft/server'; import { CustomCommandParamType, CustomCommandStatus, CommandPermissionLevel, system } from '@minecraft/server';
import { structureCollection } from '../classes/Structure/StructureCollection'; import { instanceCollection } from '../classes/Instance/InstanceCollection';
export class DeleteCommand extends Command { export class DeleteCommand extends Command {
constructor() { constructor() {
@@ -16,9 +16,9 @@ export class DeleteCommand extends Command {
} }
run(origin, instanceName) { run(origin, instanceName) {
const instance = structureCollection.get(instanceName); const instance = instanceCollection.get(instanceName);
system.run(() => { system.run(() => {
structureCollection.delete(instanceName); instanceCollection.delete(instanceName);
origin.sendMessage({ translate: 'construct.commands.delete.success', with: [instanceName] }); origin.sendMessage({ translate: 'construct.commands.delete.success', with: [instanceName] });
}); });
return { status: CustomCommandStatus.Success }; return { status: CustomCommandStatus.Success };
+2 -2
View File
@@ -1,6 +1,6 @@
import { Command } from '../classes/Commands/Command'; import { Command } from '../classes/Commands/Command';
import { CustomCommandParamType, CustomCommandStatus, CommandPermissionLevel, system } from '@minecraft/server'; import { CustomCommandParamType, CustomCommandStatus, CommandPermissionLevel, system } from '@minecraft/server';
import { structureCollection } from '../classes/Structure/StructureCollection'; import { instanceCollection } from '../classes/Instance/InstanceCollection';
export class EnableCommand extends Command { export class EnableCommand extends Command {
constructor() { constructor() {
@@ -17,7 +17,7 @@ export class EnableCommand extends Command {
} }
run(origin, instanceName, state) { run(origin, instanceName, state) {
const instance = structureCollection.get(instanceName); const instance = instanceCollection.get(instanceName);
if (state && !instance.hasLocation()) { if (state && !instance.hasLocation()) {
origin.sendMessage({ translate: 'construct.commands.error.noLocation', with: [instanceName] }); origin.sendMessage({ translate: 'construct.commands.error.noLocation', with: [instanceName] });
return void 0; return void 0;
@@ -1,6 +1,6 @@
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server';
import { Command } from '../classes/Commands/Command'; import { Command } from '../classes/Commands/Command';
import { structureCollection } from '../classes/Structure/StructureCollection'; import { instanceCollection } from '../classes/Instance/InstanceCollection';
import { Vector } from '../lib/Vector'; import { Vector } from '../lib/Vector';
export class InstanceInfoCommand extends Command { export class InstanceInfoCommand extends Command {
@@ -17,7 +17,7 @@ export class InstanceInfoCommand extends Command {
} }
run(origin, instanceName) { run(origin, instanceName) {
const instance = structureCollection.get(instanceName); const instance = instanceCollection.get(instanceName);
const message = { rawtext: [ const message = { rawtext: [
this.getHeaderText(instance), this.getHeaderText(instance),
{ text: '\n' }, { text: '\n' },
@@ -29,6 +29,8 @@ export class InstanceInfoCommand extends Command {
{ text: '\n' }, { text: '\n' },
this.getLayerText(instance), this.getLayerText(instance),
{ text: '\n' }, { text: '\n' },
this.getRenderModeText(instance),
{ text: '\n' },
this.getVerifierText(instance), this.getVerifierText(instance),
{ text: '\n' }, { text: '\n' },
this.getSizeText(instance) this.getSizeText(instance)
@@ -49,7 +51,7 @@ export class InstanceInfoCommand extends Command {
if (!instance.hasLocation()) if (!instance.hasLocation())
return { translate: 'construct.commands.instanceinfo.noLocation' }; return { translate: 'construct.commands.instanceinfo.noLocation' };
const { dimensionId, location } = instance.getLocation(); const { dimensionId, location } = instance.getLocation();
return { translate: 'construct.commands.instanceinfo.location', with: [location.toString(), dimensionId.replace('minecraft:', '')] }; return { translate: 'construct.commands.instanceinfo.location', with: [Vector.from(location).toString(), dimensionId.replace('minecraft:', '')] };
} }
getEnabledText(instance) { getEnabledText(instance) {
@@ -60,6 +62,10 @@ export class InstanceInfoCommand extends Command {
return { translate: 'construct.commands.instanceinfo.layer', with: [String(instance.getLayer()), String(instance.getMaxLayer())] }; return { translate: 'construct.commands.instanceinfo.layer', with: [String(instance.getLayer()), String(instance.getMaxLayer())] };
} }
getRenderModeText(instance) {
return { translate: 'construct.commands.instanceinfo.renderMode', with: [String(instance.getRenderMode())] };
}
getVerifierText(instance) { getVerifierText(instance) {
const verifier = instance.options.verifier; const verifier = instance.options.verifier;
return { translate: 'construct.commands.instanceinfo.verifier', with: [String(verifier.isEnabled)] }; return { translate: 'construct.commands.instanceinfo.verifier', with: [String(verifier.isEnabled)] };
@@ -67,7 +73,7 @@ export class InstanceInfoCommand extends Command {
getSizeText(instance) { getSizeText(instance) {
const bounds = instance.getBounds(); const bounds = instance.getBounds();
return { translate: 'construct.commands.instanceinfo.size', with: [bounds.max.toString(), Vector.volume(bounds.min, bounds.max).toString()] }; return { translate: 'construct.commands.instanceinfo.size', with: [Vector.from(bounds.max).toString(), Vector.volume(bounds.min, bounds.max).toString()] };
} }
} }
@@ -1,6 +1,6 @@
import { CommandPermissionLevel, CustomCommandStatus, system } from '@minecraft/server'; import { CommandPermissionLevel, CustomCommandStatus, system } from '@minecraft/server';
import { Command } from '../classes/Commands/Command'; import { Command } from '../classes/Commands/Command';
import { structureCollection } from '../classes/Structure/StructureCollection'; import { instanceCollection } from '../classes/Instance/InstanceCollection';
export class InstancesCommand extends Command { export class InstancesCommand extends Command {
constructor() { constructor() {
@@ -13,7 +13,7 @@ export class InstancesCommand extends Command {
} }
run(origin) { run(origin) {
const names = structureCollection.getInstanceNames(); const names = instanceCollection.getInstanceNames();
if (names.length === 0) if (names.length === 0)
return { status: CustomCommandStatus.Success, message: 'construct.commands.instances.empty' }; return { status: CustomCommandStatus.Success, message: 'construct.commands.instances.empty' };
const rawtext = [ const rawtext = [
@@ -21,7 +21,7 @@ export class InstancesCommand extends Command {
{ text: '\n' } { text: '\n' }
]; ];
for (const name of names) { for (const name of names) {
const instance = structureCollection.get(name); const instance = instanceCollection.get(name);
const status = this.formatStatus(instance); const status = this.formatStatus(instance);
rawtext.push({ rawtext.push({
translate: 'construct.commands.instances.row', translate: 'construct.commands.instances.row',
+2 -2
View File
@@ -1,6 +1,6 @@
import { Command } from '../classes/Commands/Command'; import { Command } from '../classes/Commands/Command';
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server';
import { structureCollection } from '../classes/Structure/StructureCollection'; import { instanceCollection } from '../classes/Instance/InstanceCollection';
export class LayerCommand extends Command { export class LayerCommand extends Command {
constructor() { constructor() {
@@ -17,7 +17,7 @@ export class LayerCommand extends Command {
} }
run(origin, instanceName, layer) { run(origin, instanceName, layer) {
const instance = structureCollection.get(instanceName); const instance = instanceCollection.get(instanceName);
const max = instance.getMaxLayer(); const max = instance.getMaxLayer();
if (layer < 0 || layer > max) { if (layer < 0 || layer > max) {
origin.sendMessage({ translate: 'construct.commands.layer.outOfBounds', with: [String(layer), instanceName, String(max)] }); origin.sendMessage({ translate: 'construct.commands.layer.outOfBounds', with: [String(layer), instanceName, String(max)] });
@@ -1,6 +1,6 @@
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, EntityComponentTypes, system } from '@minecraft/server'; import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, EntityComponentTypes, system } from '@minecraft/server';
import { Command } from '../classes/Commands/Command'; import { Command } from '../classes/Commands/Command';
import { structureCollection } from '../classes/Structure/StructureCollection'; import { instanceCollection } from '../classes/Instance/InstanceCollection';
import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin'; import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin';
import { NotAPlayerError } from '../classes/Errors/NotAPlayerError'; import { NotAPlayerError } from '../classes/Errors/NotAPlayerError';
@@ -21,7 +21,7 @@ export class MaterialsCommand extends Command {
} }
run(origin, instanceName, missing) { run(origin, instanceName, missing) {
const instance = structureCollection.get(instanceName); const instance = instanceCollection.get(instanceName);
const onlyMissing = missing === true; const onlyMissing = missing === true;
if (onlyMissing) if (onlyMissing)
this.assertIsPlayer(origin); this.assertIsPlayer(origin);
+3 -3
View File
@@ -1,7 +1,7 @@
import { Command } from '../classes/Commands/Command'; import { Command } from '../classes/Commands/Command';
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system, world } from '@minecraft/server'; import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system, world } from '@minecraft/server';
import { Vector } from '../lib/Vector'; import { Vector } from '../lib/Vector';
import { structureCollection } from '../classes/Structure/StructureCollection'; import { instanceCollection } from '../classes/Instance/InstanceCollection';
import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin'; import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin';
export class MoveCommand extends Command { export class MoveCommand extends Command {
@@ -13,7 +13,7 @@ export class MoveCommand extends Command {
{ name: 'instanceName', type: CustomCommandParamType.String } { name: 'instanceName', type: CustomCommandParamType.String }
], ],
optionalParameters: [ optionalParameters: [
{ name: 'dimensionId', type: CustomCommandParamType.Enum }, // Enum defined in PlaceCommand.js { name: 'dimensionId', type: CustomCommandParamType.Enum }, // Enum defined in PlaceCommand
{ name: 'location', type: CustomCommandParamType.Location } { name: 'location', type: CustomCommandParamType.Location }
], ],
permissionLevel: CommandPermissionLevel.Any, permissionLevel: CommandPermissionLevel.Any,
@@ -22,7 +22,7 @@ export class MoveCommand extends Command {
} }
run(origin, instanceName, dimensionId, location) { run(origin, instanceName, dimensionId, location) {
const instance = structureCollection.get(instanceName); const instance = instanceCollection.get(instanceName);
if (dimensionId === void 0 || location === void 0) { if (dimensionId === void 0 || location === void 0) {
if (!(origin instanceof PlayerCommandOrigin)) if (!(origin instanceof PlayerCommandOrigin))
return { status: CustomCommandStatus.Failure, message: 'construct.commands.move.locationRequired' }; return { status: CustomCommandStatus.Failure, message: 'construct.commands.move.locationRequired' };
@@ -1,6 +1,6 @@
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server';
import { Command } from '../classes/Commands/Command'; import { Command } from '../classes/Commands/Command';
import { structureCollection } from '../classes/Structure/StructureCollection'; import { instanceCollection } from '../classes/Instance/InstanceCollection';
export class NextLayerCommand extends Command { export class NextLayerCommand extends Command {
constructor() { constructor() {
@@ -16,7 +16,7 @@ export class NextLayerCommand extends Command {
} }
run(origin, instanceName) { run(origin, instanceName) {
const instance = structureCollection.get(instanceName); const instance = instanceCollection.get(instanceName);
instance.increaseLayer(); instance.increaseLayer();
origin.sendMessage({ translate: 'construct.commands.nextlayer.success', with: [instanceName, String(instance.getLayer())] }); origin.sendMessage({ translate: 'construct.commands.nextlayer.success', with: [instanceName, String(instance.getLayer())] });
return { status: CustomCommandStatus.Success }; return { status: CustomCommandStatus.Success };
+2 -2
View File
@@ -1,6 +1,6 @@
import { Command } from '../classes/Commands/Command'; import { Command } from '../classes/Commands/Command';
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, DimensionTypes, system, world } from '@minecraft/server'; import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, DimensionTypes, system, world } from '@minecraft/server';
import { structureCollection } from '../classes/Structure/StructureCollection'; import { instanceCollection } from '../classes/Instance/InstanceCollection';
import { InstanceExistsError } from '../classes/Errors/InstanceExistsError'; import { InstanceExistsError } from '../classes/Errors/InstanceExistsError';
import { Vector } from '../lib/Vector'; import { Vector } from '../lib/Vector';
@@ -21,7 +21,7 @@ export class PlaceCommand extends Command {
} }
run(origin, instanceName, dimensionId, location) { run(origin, instanceName, dimensionId, location) {
const instance = structureCollection.get(instanceName); const instance = instanceCollection.get(instanceName);
const flooredLocation = Vector.from(location).floor(); const flooredLocation = Vector.from(location).floor();
this.assertDimensionExists(dimensionId); this.assertDimensionExists(dimensionId);
system.run(() => { system.run(() => {
@@ -1,6 +1,6 @@
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server';
import { Command } from '../classes/Commands/Command'; import { Command } from '../classes/Commands/Command';
import { structureCollection } from '../classes/Structure/StructureCollection'; import { instanceCollection } from '../classes/Instance/InstanceCollection';
export class PrevLayerCommand extends Command { export class PrevLayerCommand extends Command {
constructor() { constructor() {
@@ -16,7 +16,7 @@ export class PrevLayerCommand extends Command {
} }
run(origin, instanceName) { run(origin, instanceName) {
const instance = structureCollection.get(instanceName); const instance = instanceCollection.get(instanceName);
instance.decreaseLayer(); instance.decreaseLayer();
origin.sendMessage({ translate: 'construct.commands.prevlayer.success', with: [instanceName, String(instance.getLayer())] }); origin.sendMessage({ translate: 'construct.commands.prevlayer.success', with: [instanceName, String(instance.getLayer())] });
return { status: CustomCommandStatus.Success }; return { status: CustomCommandStatus.Success };
+4 -4
View File
@@ -1,6 +1,6 @@
import { Command } from '../classes/Commands/Command'; import { Command } from '../classes/Commands/Command';
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server';
import { structureCollection } from '../classes/Structure/StructureCollection'; import { instanceCollection } from '../classes/Instance/InstanceCollection';
export class RenameCommand extends Command { export class RenameCommand extends Command {
constructor() { constructor() {
@@ -17,12 +17,12 @@ export class RenameCommand extends Command {
} }
run(origin, instanceName, newName) { run(origin, instanceName, newName) {
const instance = structureCollection.get(instanceName); const instance = instanceCollection.get(instanceName);
if (structureCollection.has(newName)) { if (instanceCollection.has(newName)) {
origin.sendMessage({ translate: 'construct.error.instanceExists', with: [newName] }); origin.sendMessage({ translate: 'construct.error.instanceExists', with: [newName] });
return void 0; return void 0;
} }
structureCollection.rename(instanceName, newName); instanceCollection.rename(instanceName, newName);
origin.sendMessage({ translate: 'construct.commands.rename.success', with: [instanceName, newName] }); origin.sendMessage({ translate: 'construct.commands.rename.success', with: [instanceName, newName] });
return { status: CustomCommandStatus.Success }; return { status: CustomCommandStatus.Success };
} }
+5 -6
View File
@@ -1,9 +1,9 @@
import { Command } from '../classes/Commands/Command'; import { Command } from '../classes/Commands/Command';
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system, TicksPerSecond } from '@minecraft/server'; import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system, TicksPerSecond } from '@minecraft/server';
import { InstanceFormBuilder } from '../classes/Instance/InstanceFormBuilder'; import { InstanceFormBuilder } from '../classes/Instance/InstanceFormBuilder';
import { structureCollection } from '../classes/Structure/StructureCollection'; import { instanceCollection } from '../classes/Instance/InstanceCollection';
import { StructureVerifier } from '../classes/Verifier/StructureVerifier'; import { StructureVerifier } from '../classes/Verifier/StructureVerifier';
import { StructureStatistics } from '../classes/Structure/StructureStatistics'; import { InstanceStatistics } from '../classes/Instance/InstanceStatistics';
export class StatsCommand extends Command { export class StatsCommand extends Command {
constructor() { constructor() {
@@ -19,7 +19,7 @@ export class StatsCommand extends Command {
} }
run(origin, instanceName) { run(origin, instanceName) {
const instance = structureCollection.get(instanceName); const instance = instanceCollection.get(instanceName);
if (this.structureVerifier) if (this.structureVerifier)
return { status: CustomCommandStatus.Failure, error: 'construct.commands.stats.alreadyRunning' }; return { status: CustomCommandStatus.Failure, error: 'construct.commands.stats.alreadyRunning' };
system.run(async () => { system.run(async () => {
@@ -29,10 +29,9 @@ export class StatsCommand extends Command {
} }
async getStatsMessage(instance) { async getStatsMessage(instance) {
const verifierOptions = { isEnabled: true, particleLifetime: 1*TicksPerSecond, isStandalone: true }; this.structureVerifier = StructureVerifier.standalone(instance, { particleLifetime: 1*TicksPerSecond });
this.structureVerifier = new StructureVerifier(instance, verifierOptions);
const verification = await this.structureVerifier.verifyStructure(true); const verification = await this.structureVerifier.verifyStructure(true);
const statistics = new StructureStatistics(instance, verification); const statistics = new InstanceStatistics(instance, verification);
const statsMessage = statistics.getMessage(); const statsMessage = statistics.getMessage();
this.structureVerifier = void 0; this.structureVerifier = void 0;
return statsMessage; return statsMessage;
+2 -2
View File
@@ -2,7 +2,7 @@ import { Command } from '../classes/Commands/Command';
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, EntityComponentTypes, EquipmentSlot, system } from '@minecraft/server'; import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, EntityComponentTypes, EquipmentSlot, system } from '@minecraft/server';
import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin'; import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin';
import { MENU_ITEM } from '../consts'; import { MENU_ITEM } from '../consts';
import { structureCollection } from '../classes/Structure/StructureCollection'; import { instanceCollection } from '../classes/Instance/InstanceCollection';
export class TagCommand extends Command { export class TagCommand extends Command {
constructor() { constructor() {
@@ -19,7 +19,7 @@ export class TagCommand extends Command {
} }
run(origin, instanceName) { run(origin, instanceName) {
const instance = structureCollection.get(instanceName); const instance = instanceCollection.get(instanceName);
const player = origin.getSource(); const player = origin.getSource();
const equipment = player.getComponent(EntityComponentTypes.Equippable); const equipment = player.getComponent(EntityComponentTypes.Equippable);
const itemStack = equipment?.getEquipment(EquipmentSlot.Mainhand); const itemStack = equipment?.getEquipment(EquipmentSlot.Mainhand);
+2 -2
View File
@@ -1,6 +1,6 @@
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server';
import { Command } from '../classes/Commands/Command'; import { Command } from '../classes/Commands/Command';
import { structureCollection } from '../classes/Structure/StructureCollection'; import { instanceCollection } from '../classes/Instance/InstanceCollection';
export class VerifierCommand extends Command { export class VerifierCommand extends Command {
constructor() { constructor() {
@@ -17,7 +17,7 @@ export class VerifierCommand extends Command {
} }
run(origin, instanceName, state) { run(origin, instanceName, state) {
const instance = structureCollection.get(instanceName); const instance = instanceCollection.get(instanceName);
if (state) if (state)
instance.setVerifierEnabled(true); instance.setVerifierEnabled(true);
else else
+5
View File
@@ -1,2 +1,7 @@
export const PACK_IDENTIFIER = 'construct'; export const PACK_IDENTIFIER = 'construct';
export const MENU_ITEM = 'construct:menu'; export const MENU_ITEM = 'construct:menu';
export const DEBUG_CONFIG = {
enable: false,
render_all_textures_as: { u: 736, v: 368 },
};
+157 -69
View File
@@ -1,78 +1,166 @@
/** export class Vector {
* Unknown author, with additions. constructor(x = 0, y = 0, z = 0) {
*/ this.x = x;
const isVec3Symbol = Symbol("isVec3"); this.y = y;
export function Vector(x = 0, y = 0, z = 0) { this.z = z;
if (new.target) { }
this.x = Number(x);
this.y = Number(y); static magnitude(vec) {
this.z = Number(z); return Math.sqrt(vec.x * vec.x + vec.y * vec.y + vec.z * vec.z);
} else {return { x: Number(x), y: Number(y), z: Number(z), __proto__: Vector.prototype };} }
}
Vector.magnitude = function magnitude(vec) { return Math.sqrt(vec.x * vec.x + vec.y * vec.y + vec.z * vec.z); } static normalize(vec) {
Vector.normalize = function normalize(vec) { const l = Vector.magnitude(vec); return { x: vec.x / l, y: vec.y / l, z: vec.z / l, __proto__: Vector.prototype }; } const l = Vector.magnitude(vec); return new Vector(vec.x / l, vec.y / l, vec.z / l);
Vector.cross = function crossProduct(a, b) { return { x: a.y * b.z - a.z * b.y, y: a.x * b.z - a.z * b.x, z: a.x * b.y - a.y * b.x, __proto__: Vector.prototype }; } }
Vector.dot = function dot(a, b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
Vector.angleBetween = function angleBetween(a, b) { return Math.acos(Vector.dot(a, b) / (Vector.magnitude(a) * Vector.magnitude(b))); } static cross(a, b) {
Vector.subtract = function subtract(a, b) { return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z, __proto__: Vector.prototype } }; return new Vector(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x);
Vector.add = function add(a, b) { return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z, __proto__: Vector.prototype } }; }
Vector.multiply = function multiply(vec, num) {
if (typeof num == "number") return { x: vec.x * num, y: vec.y * num, z: vec.z * num, __proto__: Vector.prototype }; static dot(a, b) {
return { x: vec.x * num.x, y: vec.y * num.y, z: vec.z * num.z, __proto__: Vector.prototype }; return a.x * b.x + a.y * b.y + a.z * b.z;
} }
Vector.isVec3 = function isVec3(vec) { return vec[isVec3Symbol] === true; }
Vector.floor = function floor(vec) { return { x: Math.floor(vec.x), y: Math.floor(vec.y), z: Math.floor(vec.z), __proto__: Vector.prototype }; } static angleBetween(a, b) {
Vector.volume = function volume(a, b) { return Math.acos(Vector.dot(a, b) / (Vector.magnitude(a) * Vector.magnitude(b)));
const [min, max] = Vector.sort(a, b); }
return (max.x - min.x) * (max.y - min.y) * (max.z - min.z);
} static subtract(a, b) {
Vector.projection = function projection(a, b) { return Vector.multiply(b, Vector.dot(a, b) / ((b.x * b.x + b.y * b.y + b.z * b.z) ** 2)); } return new Vector(a.x - b.x, a.y - b.y, a.z - b.z);
Vector.rejection = function rejection(a, b) { return Vector.subtract(a, Vector.projection(a, b)); } }
Vector.reflect = function reflect(v, n) { return Vector.subtract(v, Vector.multiply(n, 2 * Vector.dot(v, n))); }
Vector.lerp = function lerp(a, b, t) { return Vector.multiply(a, 1 - t).add(Vector.multiply(b, t)); } static add(a, b) {
Vector.distance = function distance(a, b) { return Vector.magnitude(Vector.subtract(a, b)); } return new Vector(a.x + b.x, a.y + b.y, a.z + b.z);
Vector.intersect = function intersect(maxA, minA, maxB, minB) { }
static floor(vec) {
return new Vector(Math.floor(vec.x), Math.floor(vec.y), Math.floor(vec.z));
}
static multiply(vec, num) {
if (typeof num === "number")
return new Vector(vec.x * num, vec.y * num, vec.z * num);
return new Vector(vec.x * num.x, vec.y * num.y, vec.z * num.z);
}
static volume(a, b) {
return Math.abs((a.x - b.x) * (a.y - b.y) * (a.z - b.z));
}
static projection(a, b) {
const scale = Vector.dot(a, b) / (b.x * b.x + b.y * b.y + b.z * b.z);
return new Vector(b.x * scale, b.y * scale, b.z * scale);
}
static rejection(a, b) {
const scale = Vector.dot(a, b) / (b.x * b.x + b.y * b.y + b.z * b.z);
return new Vector(a.x - b.x * scale, a.y - b.y * scale, a.z - b.z * scale);
}
static reflect(v, n) {
const scale = 2 * Vector.dot(v, n);
return new Vector(v.x - n.x * scale, v.y - n.y * scale, v.z - n.z * scale);
}
static lerp(a, b, t) {
return new Vector(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t);
}
static distance(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
const dz = a.z - b.z;
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
static intersect(maxA, minA, maxB, minB) {
return (maxA.x >= minB.x && minA.x <= maxB.x) && return (maxA.x >= minB.x && minA.x <= maxB.x) &&
(maxA.y >= minB.y && minA.y <= maxB.y) && (maxA.y >= minB.y && minA.y <= maxB.y) &&
(maxA.z >= minB.z && minA.z <= maxB.z); (maxA.z >= minB.z && minA.z <= maxB.z);
} }
Vector.from = function from(object) {
if (Vector.isVec3(object)) return object; static from(object) {
if (Array.isArray(object)) return new Vector(object[0], object[1], object[2]); if (object instanceof Vector)
return object;
if (Array.isArray(object))
return new Vector(object[0], object[1], object[2]);
const { x = 0, y = 0, z = 0 } = object ?? {}; const { x = 0, y = 0, z = 0 } = object ?? {};
return { x: Number(x), y: Number(y), z: Number(z), __proto__: Vector.prototype }; return new Vector(x, y, z);
} }
Vector.sort = function sort(vec1, vec2) {
static sort(vec1, vec2) {
const [x1, x2] = vec1.x < vec2.x ? [vec1.x, vec2.x] : [vec2.x, vec1.x]; const [x1, x2] = vec1.x < vec2.x ? [vec1.x, vec2.x] : [vec2.x, vec1.x];
const [y1, y2] = vec1.y < vec2.y ? [vec1.y, vec2.y] : [vec2.y, vec1.y]; const [y1, y2] = vec1.y < vec2.y ? [vec1.y, vec2.y] : [vec2.y, vec1.y];
const [z1, z2] = vec1.z < vec2.z ? [vec1.z, vec2.z] : [vec2.z, vec1.z]; const [z1, z2] = vec1.z < vec2.z ? [vec1.z, vec2.z] : [vec2.z, vec1.z];
return [{ x: x1, y: y1, z: z1, __proto__: Vector.prototype }, { x: x2, y: y2, z: z2, __proto__: Vector.prototype }]; return [new Vector(x1, y1, z1), new Vector(x2, y2, z2)];
} }
Vector.up = { x: 0, y: 1, z: 0, __proto__: Vector.prototype };
Vector.down = { x: 0, y: -1, z: 0, __proto__: Vector.prototype }; distance(vec) { return Vector.distance(this, vec); }
Vector.right = { x: 1, y: 0, z: 0, __proto__: Vector.prototype }; lerp(vec, t) { return Vector.lerp(this, vec, t); }
Vector.left = { x: -1, y: 0, z: 0, __proto__: Vector.prototype }; projection(vec) { return Vector.projection(this, vec); }
Vector.forward = { x: 0, y: 0, z: 1, __proto__: Vector.prototype }; reflect(vec) { return Vector.reflect(this, vec); }
Vector.backward = { x: 0, y: 0, z: -1, __proto__: Vector.prototype }; rejection(vec) { return Vector.rejection(this, vec); }
Vector.zero = { x: 0, y: 0, z: 0, __proto__: Vector.prototype }; cross(vec) { return Vector.cross(this, vec); }
Vector.prototype = { dot(vec) { return Vector.dot(this, vec); }
distance(vec) { return Vector.distance(this, vec); }, floor() { return Vector.floor(this); }
lerp(vec, t) { return Vector.lerp(this, vec, t); }, volume(vec) { return Vector.volume(this, vec); }
projection(vec) { return Vector.projection(this, vec); }, add(vec) { return Vector.add(this, vec); }
reflect(vec) { return Vector.reflect(this, vec); }, subtract(vec) { return Vector.subtract(this, vec); }
rejection(vec) { return Vector.rejection(this, vec); }, multiply(num) { return Vector.multiply(this, num); }
cross(vec) { return Vector.cross(this, vec); },
dot(vec) { return Vector.dot(this, vec); }, set(x, y, z) {
floor() { return Vector.floor(this); }, this.x = x;
volume(vec) { return Vector.volume(this, vec); }, this.y = y;
add(vec) { return Vector.add(this, vec); }, this.z = z;
subtract(vec) { return Vector.subtract(this, vec); }, return this;
multiply(num) { return Vector.multiply(this, num); }, }
get length() { return Vector.magnitude(this); },
get normalized() { return Vector.normalize(this); }, setFrom(vec) {
x: 0, this.x = vec.x;
y: 0, this.y = vec.y;
z: 0, this.z = vec.z;
[isVec3Symbol]: true, return this;
}
addInPlace(vec) {
this.x += vec.x;
this.y += vec.y;
this.z += vec.z;
return this;
}
subtractInPlace(vec) {
this.x -= vec.x;
this.y -= vec.y;
this.z -= vec.z;
return this;
}
floorInPlace() {
this.x = Math.floor(this.x);
this.y = Math.floor(this.y);
this.z = Math.floor(this.z);
return this;
}
multiplyInPlace(num) {
if (typeof num === "number") {
this.x *= num; this.y *= num; this.z *= num;
return this;
}
this.x *= num.x; this.y *= num.y; this.z *= num.z;
return this;
}
get length() { return Vector.magnitude(this); }
get normalized() { return Vector.normalize(this); }
toString() { return `<${this.x}, ${this.y}, ${this.z}>`; } toString() { return `<${this.x}, ${this.y}, ${this.z}>`; }
} }
Vector.up = Object.freeze(new Vector(0, 1, 0));
Vector.down = Object.freeze(new Vector(0, -1, 0));
Vector.north = Object.freeze(new Vector(0, 0, -1));
Vector.south = Object.freeze(new Vector(0, 0, 1));
Vector.east = Object.freeze(new Vector(1, 0, 0));
Vector.west = Object.freeze(new Vector(-1, 0, 0));
Vector.zero = Object.freeze(new Vector(0, 0, 0));
+4 -2
View File
@@ -1,13 +1,15 @@
// Setup // Setup
import './classes/Builder/Builders'; import './classes/Builder/Builders';
import './classes/BlockChangeWatcher';
// Options // Options
import './options/easyPlace'; import './options/easyPlace';
import './options/fastEasyPlace'; import './options/fastEasyPlace';
import './options/materialGrabber'; import './options/materialGrabber';
import './options/defaultRenderMode';
// Menu item handler // Interaction Handling
import './classes/MenuItemHandler'; import './classes/MenuItemInteractions';
// Commands // Commands
import './commands/ConstructCommand'; import './commands/ConstructCommand';
@@ -0,0 +1,17 @@
import { BuilderChoiceOption } from '../classes/Builder/BuilderChoiceOption';
import { RENDER_MODE_LABELS, RENDER_MODE_ORDER, RenderMode } from '../classes/Enums/RenderMode';
const builderOption = new BuilderChoiceOption({
identifier: 'defaultRenderMode',
displayName: { translate: 'construct.option.defaultrendermode.name' },
description: { translate: 'construct.option.defaultrendermode.description' },
choices: RENDER_MODE_ORDER,
choiceLabels: RENDER_MODE_ORDER.map(mode => ({ translate: RENDER_MODE_LABELS[mode] })),
defaultValue: RenderMode.Default
});
export function getDefaultRenderMode(playerId) {
if (!playerId)
return RenderMode.Default;
return builderOption.getValue(playerId);
}
+20 -37
View File
@@ -1,6 +1,7 @@
import { BuilderOption } from '../classes/Builder/BuilderOption'; import { BuilderOption } from '../classes/Builder/BuilderOption';
import { BlockPermutation, EntityComponentTypes, EquipmentSlot, GameMode, ItemStack, system, world } from '@minecraft/server'; import { BlockPermutation, EntityComponentTypes, EquipmentSlot, GameMode, ItemStack, system, world } from '@minecraft/server';
import { structureCollection } from '../classes/Structure/StructureCollection'; import { StructureBlockConverter } from '../classes/Structure/StructureBlockConverter';
import { instanceCollection } from '../classes/Instance/InstanceCollection';
import { bannedBlocks, bannedToValidBlockMap, whitelistedBlockStates, resetToBlockStates, bannedDimensionBlocks, blockIdToItemStackMap } from './easyPlaceConversions'; import { bannedBlocks, bannedToValidBlockMap, whitelistedBlockStates, resetToBlockStates, bannedDimensionBlocks, blockIdToItemStackMap } from './easyPlaceConversions';
import { fetchMatchingItemSlot, placeBlock } from '../utils'; import { fetchMatchingItemSlot, placeBlock } from '../utils';
import { Builders } from '../classes/Builder/Builders'; import { Builders } from '../classes/Builder/Builders';
@@ -49,7 +50,7 @@ world.beforeEvents.playerPlaceBlock.subscribe(onPlayerPlaceBlock);
function onPlayerPlaceBlock(event) { function onPlayerPlaceBlock(event) {
const { player, block } = event; const { player, block } = event;
if (!player || !block || !builderOption.isEnabled(player.id) || !isHoldingActionItem(player)) return; if (!player || !block || !builderOption.isEnabled(player.id) || !isHoldingActionItem(player)) return;
const structureBlock = structureCollection.fetchStructureBlock(block.dimension.id, block.location); const structureBlock = instanceCollection.fetchStructureBlock(block.dimension.id, block.location);
if (!structureBlock) if (!structureBlock)
return; return;
tryPlaceBlock(event, player, block, structureBlock); tryPlaceBlock(event, player, block, structureBlock);
@@ -65,13 +66,17 @@ function isHoldingActionItem(player) {
function tryPlaceBlock(event, player, block, structureBlock) { function tryPlaceBlock(event, player, block, structureBlock) {
if (shouldPreventAction(player, structureBlock)) if (shouldPreventAction(player, structureBlock))
return preventAction(event, player); return preventAction(event, player);
structureBlock = tryConvertBannedToValidBlock(structureBlock); const gameMode = player.getGameMode();
if (player.getGameMode() === GameMode.Creative) { let permutation = StructureBlockConverter.fromBannedBlockToValidPermutation(structureBlock);
if (gameMode === GameMode.Survival)
permutation = StructureBlockConverter.toDefaultState(permutation);
if (!isSupported(block, permutation))
return preventAction(event, player);
if (gameMode === GameMode.Creative) {
event.cancel = true; event.cancel = true;
placeBlock(player, block, structureBlock); placeBlock(player, block, permutation);
} else if (player.getGameMode() === GameMode.Survival) { } else if (gameMode === GameMode.Survival) {
structureBlock = tryConvertToDefaultState(structureBlock); tryPlaceBlockSurvival(event, player, block, permutation);
tryPlaceBlockSurvival(event, player, block, structureBlock);
} }
} }
@@ -87,7 +92,7 @@ function preventAction(event, player) {
} }
function isBannedBlock(player, structureBlock) { function isBannedBlock(player, structureBlock) {
const blockId = structureBlock.type.id.replace('minecraft:', ''); const blockId = structureBlock.typeId.replace('minecraft:', '');
if (bannedBlocks.includes(blockId)) if (bannedBlocks.includes(blockId))
return true; return true;
if (bannedDimensionBlocks[player.dimension.id.replace('minecraft:', '')]?.includes(blockId)) if (bannedDimensionBlocks[player.dimension.id.replace('minecraft:', '')]?.includes(blockId))
@@ -95,46 +100,24 @@ function isBannedBlock(player, structureBlock) {
const allowedStates = whitelistedBlockStates[blockId]; const allowedStates = whitelistedBlockStates[blockId];
if (allowedStates) { if (allowedStates) {
for (const [stateKey, stateValue] of Object.entries(allowedStates)) { for (const [stateKey, stateValue] of Object.entries(allowedStates)) {
if (structureBlock.getState(stateKey) !== stateValue) if (structureBlock.states[stateKey] !== stateValue)
return true; return true;
} }
} }
return false; return false;
} }
function tryConvertBannedToValidBlock(structureBlock) { function isSupported(worldBlock, permutationToPlace) {
const blockId = structureBlock.type.id.replace('minecraft:', ''); return worldBlock.canPlace(permutationToPlace);
if (Object.keys(bannedToValidBlockMap).includes(blockId))
return BlockPermutation.resolve(bannedToValidBlockMap[blockId], structureBlock.getAllStates());
if (blockId === "bubble_column" && structureBlock.isWaterlogged)
return BlockPermutation.resolve('minecraft:water');
return structureBlock;
} }
function tryConvertToDefaultState(structureBlock) { function tryPlaceBlockSurvival(event, player, block, permutation) {
const newStates = {}; const placeableItemStack = StructureBlockConverter.toPlaceableItemStack(permutation);
for (const [stateKey, stateValue] of Object.entries(structureBlock.getAllStates())) {
if (resetToBlockStates[stateKey] !== void 0 && stateValue !== resetToBlockStates[stateKey])
newStates[stateKey] = resetToBlockStates[stateKey];
else
newStates[stateKey] = stateValue;
}
return BlockPermutation.resolve(structureBlock.type.id, newStates);
}
function tryPlaceBlockSurvival(event, player, block, structureBlock) {
const placeableItemStack = getPlaceableItemStack(structureBlock);
const itemSlotToUse = fetchMatchingItemSlot(player, placeableItemStack?.typeId); const itemSlotToUse = fetchMatchingItemSlot(player, placeableItemStack?.typeId);
if (itemSlotToUse) { if (itemSlotToUse) {
event.cancel = true; event.cancel = true;
placeBlock(player, block, structureBlock, itemSlotToUse); placeBlock(player, block, permutation, itemSlotToUse);
} else { } else {
preventAction(event, player); preventAction(event, player);
} }
} }
function getPlaceableItemStack(structureBlock) {
const blockId = structureBlock.type.id.replace('minecraft:', '');
const newItemId = blockIdToItemStackMap[blockId];
return newItemId ? new ItemStack(newItemId) : structureBlock.getItemStack();
}
+26 -41
View File
@@ -6,6 +6,7 @@ import { placeBlock, fetchMatchingItemSlot } from '../utils';
import { Raycaster } from '../classes/Raycaster'; import { Raycaster } from '../classes/Raycaster';
import { Builders } from '../classes/Builder/Builders'; import { Builders } from '../classes/Builder/Builders';
import { Vector } from '../lib/Vector'; import { Vector } from '../lib/Vector';
import { StructureBlockConverter } from '../classes/Structure/StructureBlockConverter';
const locationsPlacedLastTick = new Set(); const locationsPlacedLastTick = new Set();
const ACTION_ITEM = 'construct:easy_place'; const ACTION_ITEM = 'construct:easy_place';
@@ -78,14 +79,15 @@ function onPlayerInteractWithBlock(event) {
const { player, block, isFirstEvent } = event; const { player, block, isFirstEvent } = event;
if (!player || !isFirstEvent || !block || !builderOption.isEnabled(player.id) || !isHoldingActionItem(player)) return; if (!player || !isFirstEvent || !block || !builderOption.isEnabled(player.id) || !isHoldingActionItem(player)) return;
preventAction(event, player); preventAction(event, player);
processEasyPlace(player);
} }
function processEasyPlace(player) { function processEasyPlace(player) {
if (!player || !isHoldingActionItem(player)) return; if (!player || !isHoldingActionItem(player)) return;
const structureBlock = Raycaster.getTargetedStructureBlock(player, { isFirst: true }); const hit = Raycaster.getTargetedStructureBlock(player, { isFirst: true });
if (!structureBlock) if (!hit)
return; return;
const worldBlock = player.dimension.getBlock(structureBlock.location); const worldBlock = player.dimension.getBlock(hit.location);
if (!worldBlock) if (!worldBlock)
return; return;
if (locationsPlacedLastTick.has(JSON.stringify(worldBlock.location))) if (locationsPlacedLastTick.has(JSON.stringify(worldBlock.location)))
@@ -94,7 +96,7 @@ function processEasyPlace(player) {
system.runTimeout(() => { system.runTimeout(() => {
locationsPlacedLastTick.delete(JSON.stringify(worldBlock.location)); locationsPlacedLastTick.delete(JSON.stringify(worldBlock.location));
}, 2); }, 2);
tryPlaceBlock(player, worldBlock, structureBlock.permutation); tryPlaceBlock(player, worldBlock, hit.block);
} }
function preventAction(event, player) { function preventAction(event, player) {
@@ -116,13 +118,18 @@ function isHoldingActionItem(player) {
} }
function tryPlaceBlock(player, worldBlock, structureBlock) { function tryPlaceBlock(player, worldBlock, structureBlock) {
if (isBannedBlock(player, structureBlock) || !locationIsPlaceable(player, worldBlock)) return; if (isBannedBlock(player, structureBlock) || !locationIsPlaceable(player, worldBlock))
structureBlock = tryConvertBannedToValidBlock(structureBlock); return;
if (player.getGameMode() === GameMode.Creative) { const gameMode = player.getGameMode();
placeBlock(player, worldBlock, structureBlock); let permutation = StructureBlockConverter.fromBannedBlockToValidPermutation(structureBlock);
} else if (player.getGameMode() === GameMode.Survival) { if (gameMode === GameMode.Survival)
structureBlock = tryConvertToDefaultState(structureBlock); permutation = StructureBlockConverter.toDefaultState(permutation);
tryPlaceBlockSurvival(player, worldBlock, structureBlock); if (!isSupported(worldBlock, permutation))
return;
if (gameMode === GameMode.Creative) {
placeBlock(player, worldBlock, permutation);
} else if (gameMode === GameMode.Survival) {
tryPlaceBlockSurvival(player, worldBlock, permutation);
} }
} }
@@ -130,10 +137,10 @@ function locationIsPlaceable(player, worldBlock) {
return (worldBlock.isAir || worldBlock.isLiquid) && !isBlockInsidePlayer(player, worldBlock); return (worldBlock.isAir || worldBlock.isLiquid) && !isBlockInsidePlayer(player, worldBlock);
} }
function isBannedBlock(player, structureBlock) { export function isBannedBlock(player, structureBlock) {
if (!structureBlock) if (!structureBlock)
return true; return true;
const blockId = structureBlock.type.id.replace('minecraft:', ''); const blockId = structureBlock.typeId.replace('minecraft:', '');
if (bannedBlocks.includes(blockId)) if (bannedBlocks.includes(blockId))
return true; return true;
if (bannedDimensionBlocks[player.dimension.id.replace('minecraft:', '')]?.includes(blockId)) if (bannedDimensionBlocks[player.dimension.id.replace('minecraft:', '')]?.includes(blockId))
@@ -141,44 +148,22 @@ function isBannedBlock(player, structureBlock) {
const allowedStates = whitelistedBlockStates[blockId]; const allowedStates = whitelistedBlockStates[blockId];
if (allowedStates) { if (allowedStates) {
for (const [stateKey, stateValue] of Object.entries(allowedStates)) { for (const [stateKey, stateValue] of Object.entries(allowedStates)) {
if (structureBlock.getState(stateKey) !== stateValue) if (structureBlock.states[stateKey] !== stateValue)
return true; return true;
} }
} }
return false; return false;
} }
function tryConvertBannedToValidBlock(structureBlock) { function isSupported(worldBlock, permutationToPlace) {
const blockId = structureBlock.type.id.replace('minecraft:', ''); return worldBlock.canPlace(permutationToPlace);
if (Object.keys(bannedToValidBlockMap).includes(blockId))
return BlockPermutation.resolve(bannedToValidBlockMap[blockId], structureBlock.getAllStates());
if (blockId === "bubble_column" && structureBlock.isWaterlogged)
return BlockPermutation.resolve('minecraft:water');
return structureBlock;
} }
function tryConvertToDefaultState(structureBlock) { function tryPlaceBlockSurvival(player, block, permutation) {
const newStates = {}; const placeableItemStack = StructureBlockConverter.toPlaceableItemStack(permutation);
for (const [stateKey, stateValue] of Object.entries(structureBlock.getAllStates())) {
if (resetToBlockStates[stateKey] !== void 0 && stateValue !== resetToBlockStates[stateKey])
newStates[stateKey] = resetToBlockStates[stateKey];
else
newStates[stateKey] = stateValue;
}
return BlockPermutation.resolve(structureBlock.type.id, newStates);
}
function tryPlaceBlockSurvival(player, block, structureBlock) {
const placeableItemStack = getPlaceableItemStack(structureBlock);
const itemSlotToUse = fetchMatchingItemSlot(player, placeableItemStack?.typeId); const itemSlotToUse = fetchMatchingItemSlot(player, placeableItemStack?.typeId);
if (itemSlotToUse) if (itemSlotToUse)
placeBlock(player, block, structureBlock, itemSlotToUse); placeBlock(player, block, permutation, itemSlotToUse);
}
function getPlaceableItemStack(structureBlock) {
const blockId = structureBlock.type.id.replace('minecraft:', '');
const newItemId = blockIdToItemStackMap[blockId];
return newItemId ? new ItemStack(newItemId) : structureBlock.getItemStack();
} }
function isBlockInsidePlayer(player, worldBlock) { function isBlockInsidePlayer(player, worldBlock) {

Some files were not shown because too many files have changed in this diff Show More