From 25cd1b9c547850a5cf56969c4e541e0df18a157d Mon Sep 17 00:00:00 2001
From: Roujel Williams <sisilicon28@gmail.com>
Date: Sun, 3 Nov 2024 23:54:46 -0500
Subject: [PATCH] Improved simple pattern/mask logic; Old PE blocks can once
 again be used in simple block patterns; Block change count more accurate with
 simple block patterns; Added support for simple negate masks

---
 src/server/modules/mask.ts      | 55 ++++++++++++++++++++++++++-------
 src/server/modules/pattern.ts   | 19 +++---------
 src/server/shapes/base_shape.ts |  2 +-
 src/server/util.ts              | 23 +++++++++++++-
 4 files changed, 72 insertions(+), 27 deletions(-)

diff --git a/src/server/modules/mask.ts b/src/server/modules/mask.ts
index 5fca3d290..87535bef0 100644
--- a/src/server/modules/mask.ts
+++ b/src/server/modules/mask.ts
@@ -1,4 +1,4 @@
-import { Vector3, BlockPermutation } from "@minecraft/server";
+import { Vector3, BlockPermutation, BlockFilter } from "@minecraft/server";
 import { CustomArgType, commandSyntaxError, Vector } from "@notbeer-api";
 import { Token } from "./extern/tokenizr.js";
 import {
@@ -12,13 +12,14 @@ import {
     parsedBlock,
     blockPermutation2ParsedBlock,
     BlockUnit,
-    parsedBlock2CommandArg,
+    parsedBlock2BlockPermutation,
 } from "./block_parsing.js";
+import { iterateBlockPermutations } from "server/util.js";
 
 export class Mask implements CustomArgType {
     private condition: MaskNode;
     private stringObj = "";
-    private simpleCache: string[];
+    private simpleCache: BlockFilter;
 
     constructor(mask = "") {
         if (mask) {
@@ -109,19 +110,51 @@ export class Mask implements CustomArgType {
     }
 
     isSimple() {
-        return !this.condition || this.condition instanceof BlockMask || (this.condition instanceof ChainMask && this.condition.nodes.every((node) => node instanceof BlockMask));
+        const root = this.condition;
+        const child = root.nodes[0];
+        return (
+            !root ||
+            root instanceof BlockMask ||
+            (root instanceof ChainMask && root.nodes.every((node) => node instanceof BlockMask)) ||
+            (root instanceof NegateMask && (child instanceof BlockMask || (child instanceof ChainMask && child.nodes.every((node) => node instanceof BlockMask))))
+        );
     }
 
-    getSimpleForCommandArgs() {
-        if (!this.simpleCache) {
-            if (this.condition instanceof BlockMask) {
-                this.simpleCache = [parsedBlock2CommandArg(this.condition.block)];
-            } else if (this.condition instanceof ChainMask) {
-                this.simpleCache = this.condition.nodes.map((node) => parsedBlock2CommandArg((<BlockMask>node).block));
+    getSimpleBlockFilter() {
+        if (this.simpleCache) return this.simpleCache;
+
+        const addToFilter = (block: parsedBlock, types: string[], perms: BlockPermutation[]) => {
+            const perm = parsedBlock2BlockPermutation(block);
+            if (block.states != null) {
+                const test = Array.from(block.states.entries());
+                for (const states of iterateBlockPermutations(block.id)) {
+                    if (!test.every(([key, value]) => states[key] === value)) continue;
+                    perms.push(BlockPermutation.resolve(block.id, states));
+                }
             } else {
-                this.simpleCache = [];
+                types.push(perm.type.id);
             }
+        };
+
+        const includeTypes: string[] = [];
+        const excludeTypes: string[] = [];
+        const includePerms: BlockPermutation[] = [];
+        const excludePerms: BlockPermutation[] = [];
+        this.simpleCache = {};
+
+        if (this.condition instanceof BlockMask) addToFilter(this.condition.block, includeTypes, includePerms);
+        else if (this.condition instanceof ChainMask) this.condition.nodes.forEach((node) => addToFilter((<BlockMask>node).block, includeTypes, includePerms));
+        else if (this.condition instanceof NegateMask) {
+            const negated = this.condition.nodes[0];
+            if (negated instanceof BlockMask) addToFilter(negated.block, excludeTypes, excludePerms);
+            else if (negated instanceof ChainMask) negated.nodes.forEach((node) => addToFilter((<BlockMask>node).block, excludeTypes, excludePerms));
         }
+
+        if (includeTypes.length) this.simpleCache.includeTypes = includeTypes;
+        if (excludeTypes.length) this.simpleCache.excludeTypes = excludeTypes;
+        if (includePerms.length) this.simpleCache.includePermutations = includePerms;
+        if (excludePerms.length) this.simpleCache.excludePermutations = excludePerms;
+
         return this.simpleCache;
     }
 
diff --git a/src/server/modules/pattern.ts b/src/server/modules/pattern.ts
index b162642a3..46a7679dd 100644
--- a/src/server/modules/pattern.ts
+++ b/src/server/modules/pattern.ts
@@ -1,4 +1,4 @@
-import { Vector3, BlockPermutation, Player, Dimension } from "@minecraft/server";
+import { Vector3, BlockPermutation, Player, Dimension, BlockVolume } from "@minecraft/server";
 import { CustomArgType, commandSyntaxError, Vector, Server } from "@notbeer-api";
 import { PlayerSession } from "server/sessions.js";
 import { wrap } from "server/util.js";
@@ -16,7 +16,6 @@ import {
     blockPermutation2ParsedBlock,
     parsedBlock2BlockPermutation,
     BlockUnit,
-    parsedBlock2CommandArg,
 } from "./block_parsing.js";
 import { Cardinal } from "./directions.js";
 import { Mask } from "./mask.js";
@@ -31,7 +30,7 @@ interface patternContext {
 export class Pattern implements CustomArgType {
     private block: PatternNode;
     private stringObj = "";
-    private simpleCache: string;
+    private simpleCache: BlockPermutation;
 
     private context = {} as patternContext;
 
@@ -148,20 +147,12 @@ export class Pattern implements CustomArgType {
     fillSimpleArea(dimension: Dimension, start: Vector3, end: Vector3, mask?: Mask) {
         if (!this.simpleCache) {
             if (this.block instanceof BlockPattern) {
-                this.simpleCache = parsedBlock2CommandArg(this.block.block);
+                this.simpleCache = parsedBlock2BlockPermutation(this.block.block);
             } else if (this.block instanceof ChainPattern) {
-                this.simpleCache = parsedBlock2CommandArg((<BlockPattern>this.block.nodes[0]).block);
+                this.simpleCache = parsedBlock2BlockPermutation((<BlockPattern>this.block.nodes[0]).block);
             }
         }
-        const command = `fill ${start.x} ${start.y} ${start.z} ${end.x} ${end.y} ${end.z} ${this.simpleCache}`;
-        const maskArgs = mask?.getSimpleForCommandArgs();
-        let successCount = 0;
-        if (maskArgs?.length) {
-            maskArgs.forEach((m) => (successCount += dimension.runCommand(command + ` replace ${m}`).successCount));
-        } else {
-            successCount += dimension.runCommand(command).successCount;
-        }
-        return !!successCount;
+        return dimension.fillBlocks(new BlockVolume(start, end), this.simpleCache, { blockFilter: mask?.getSimpleBlockFilter() }).getCapacity();
     }
 
     getSimpleBlockFill() {
diff --git a/src/server/shapes/base_shape.ts b/src/server/shapes/base_shape.ts
index 1edd9723a..d90ed3b23 100644
--- a/src/server/shapes/base_shape.ts
+++ b/src/server/shapes/base_shape.ts
@@ -268,7 +268,7 @@ export abstract class Shape {
                         const [min, max] = block;
                         const volume = regionVolume(min, max);
                         if (Jobs.inContext()) while (!Jobs.loadBlock(min)) yield sleep(1);
-                        if (pattern.fillSimpleArea(dimension, min, max, mask)) count += volume;
+                        count += pattern.fillSimpleArea(dimension, min, max, mask);
                         yield Jobs.setProgress(progress / blocksAffected);
                         progress += volume;
                     }
diff --git a/src/server/util.ts b/src/server/util.ts
index e0913c513..01022dbae 100644
--- a/src/server/util.ts
+++ b/src/server/util.ts
@@ -1,4 +1,4 @@
-import { Block, Vector3, Dimension, Entity, Player, RawMessage, BlockComponentTypes } from "@minecraft/server";
+import { Block, Vector3, Dimension, Entity, Player, RawMessage, BlockComponentTypes, BlockPermutation, BlockStates } from "@minecraft/server";
 import { Server, RawText, Vector } from "@notbeer-api";
 import config from "config.js";
 
@@ -84,6 +84,27 @@ export function blockHasNBTData(block: Block) {
     return components.some((component) => !!block.getComponent(component)) || nbt_blocks.includes(block.typeId);
 }
 
+/**
+ * Iterates through every possible block permutation for a specified block type.
+ */
+export function* iterateBlockPermutations(blockType: string) {
+    const perm = BlockPermutation.resolve(blockType);
+    const properties = Object.keys(perm.getAllStates());
+    const values = properties.map((p) => BlockStates.get(p).validValues);
+
+    function* combine(current: any[], depth: number): Generator<Record<string, any>, void> {
+        if (depth === values.length) {
+            yield Object.fromEntries(current.map((value, i) => [properties[i], value]));
+            return;
+        }
+
+        for (let i = 0; i < values[depth].length; i++) {
+            yield* combine(current.concat(values[depth][i]), depth + 1);
+        }
+    }
+    yield* combine([], 0);
+}
+
 /**
  * Converts a location object to a string.
  * @param loc The object to convert