Skip to content

Lookup updates #109

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions src/gdpc/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .nbt_tools import nbtToSnbt
from .block_state_tools import transformAxis, transformFacing, transformRotation

BlockName = str

@dataclass
class Block:
Expand All @@ -38,7 +39,7 @@ class Block:
# - type="bottom"/"top" (e.g. slabs) (note that slabs can also have type="double"!)
# - half="bottom"/"top" (e.g. stairs) ("half" is also used for other purposes, see e.g. doors)

id: Optional[str] = "minecraft:stone"
id: Optional[BlockName] = "minecraft:stone"
states: Dict[str, str] = field(default_factory=dict)
data: Optional[str] = None

Expand All @@ -65,13 +66,11 @@ def transformed(self, rotation: int = 0, flip: Vec3bLike = bvec3()):
def stateString(self):
"""Returns a string containing the block states of this block, including the outer brackets."""
stateString = ",".join([f"{key}={value}" for key, value in self.states.items()])
return "" if stateString == "" else f"[{stateString}]"
return f"[{stateString}]" if stateString else ""


def __str__(self):
if not self.id:
return ""
return self.id + self.stateString() + (self.data if self.data else "")
return self.id + self.stateString() + (self.data or "") if self.id else ""


def __repr__(self):
Expand Down
117 changes: 93 additions & 24 deletions src/gdpc/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,73 @@
world through the GDMC HTTP interface"""


from typing import Dict, Sequence, Union, Optional, List, Iterable
from numbers import Integral
from concurrent import futures
from contextlib import contextmanager
from copy import copy, deepcopy
import random
from concurrent import futures
from glm import ivec3
import logging
from numbers import Integral
import random
from typing import Dict, Iterable, List, Optional, Sequence, Union

from typing_extensions import Protocol

import numpy as np
from glm import ivec3

from .utils import eagerAll, OrderedByLookupDict
from .vector_tools import Vec3iLike, Rect, Box, addY, dropY
from .transform import Transform, TransformLike, toTransform
from .block import Block, transformedBlockOrPalette
from . import interface
from .block import Block, BlockName, transformedBlockOrPalette
from .model import Model
from .transform import Transform, TransformLike, toTransform
from .utils import OrderedByLookupDict, eagerAll
from .vector_tools import ZERO_3D, Box, Rect, Vec3iLike, dropY
from .world_slice import WorldSlice


logger = logging.getLogger(__name__)


class Editor:
class BlockGetterMixin(Protocol):

@property
def size(self) -> ivec3 :...

def getBlock(self, position: Vec3iLike) -> Optional[Block]: ...

def getBlocks(self, box: Box):
"""Returns a Model containing a cuboid of blocks, as defined by box."""
if box.getOriginDiagonal > self.size:
# FIXME: Out-of-bounds handling
raise NotImplementedError()

return Model(box.size, [self.getBlock(v) for v in box.inner])

class BlockPlacerMixin(Protocol):
def placeBlock(self,
position: Union[Vec3iLike, Iterable[Vec3iLike]],
block: Union[Block, Sequence[Block]],
replace: Optional[Union[BlockName, List[BlockName]]] = None
): ...

def placeBlocks(self,
source: BlockGetterMixin,
destination_target: Box = None, # if unspecified, assumes equal to source
source_target: Box = None, # if unspecified, assumes equal to destination
):
if destination_target is None:
destination_target = Box(size=source.size)

if source_target is None:
source_target = destination_target

if destination_target.size < source_target.size or source.size < source_target.getOriginDiagonal():
# FIXME: Out-of-bounds handling
raise NotImplementedError()

for source_v, destination_v in zip(source_target.inner, destination_target.inner):
self.placeBlock(destination_v, source.getBlock(source_v))


class Editor(BlockGetterMixin, BlockPlacerMixin):
"""Provides a high-level functions to interact with the Minecraft world through the GDMC HTTP
interface.

Expand Down Expand Up @@ -57,11 +101,11 @@ def __init__(

self._buffering = buffering
self._bufferLimit = bufferLimit
self._buffer: Dict[ivec3,Block] = {}
self._buffer: Dict[Vec3iLike,Block] = {}
self._commandBuffer: List[str] = []

self._caching = caching
self._cache = OrderedByLookupDict[ivec3,Block](cacheLimit)
self._cache = OrderedByLookupDict[Vec3iLike,Block](cacheLimit)

self._multithreading = False
self._multithreadingWorkers = multithreadingWorkers
Expand All @@ -88,14 +132,33 @@ def __del__(self):
# actually shut down yet. For safety, the last buffer flush must be done on the main thread.
self.flushBuffer()

@property
def offset(self):
"""An alias for the Editor's translation."""
return self.transform.translation

@offset.setter
def offset(self, value: Vec3iLike):
self.transform.translation = value

@property
def size(self):
"""An alias for the size of the build area."""
return self.getBuildArea().size

@size.setter
def size(self, size: Vec3iLike):
build_area = self.getBuildArea()
build_area.size = size
self.setBuildArea(build_area)

@property
def transform(self):
"""This editor's local coordinate transform (used for block placement and retrieval)"""
return self._transform

@transform.setter
def transform(self, value: Union[Transform, ivec3]):
def transform(self, value: Union[Transform, Vec3iLike]):
self._transform = toTransform(value)

@property
Expand Down Expand Up @@ -336,7 +399,7 @@ def getBlock(self, position: Vec3iLike):
def getBlockGlobal(self, position: Vec3iLike):
"""Returns the block at [position], ignoring self.transform.\n
If the given coordinates are invalid, returns Block("minecraft:void_air")."""
_position = ivec3(*position)
_position = Vec3iLike(*position)

if self.caching:
block = self._cache.get(_position)
Expand Down Expand Up @@ -376,7 +439,7 @@ def getBiomeGlobal(self, position: Vec3iLike):
if (
self._worldSlice is not None and
self._worldSlice.box.contains(position) and
not self._worldSliceDecay[tuple(ivec3(position) - self._worldSlice.box.offset)]
not self._worldSliceDecay[tuple(Vec3iLike(position) - self._worldSlice.box.offset)]
):
return self._worldSlice.getBiomeGlobal(position)

Expand All @@ -387,7 +450,7 @@ def placeBlock(
self,
position: Union[Vec3iLike, Iterable[Vec3iLike]],
block: Union[Block, Sequence[Block]],
replace: Optional[Union[str, List[str]]] = None
replace: Optional[Union[BlockName, List[BlockName]]] = None
):
"""Places <block> at <position>.\n
<position> is interpreted as local to the coordinate system defined by self.transform.\n
Expand All @@ -405,7 +468,7 @@ def placeBlockGlobal(
self,
position: Union[Vec3iLike, Iterable[Vec3iLike]],
block: Union[Block, Sequence[Block]],
replace: Optional[Union[str, Iterable[str]]] = None
replace: Optional[Union[BlockName, Iterable[BlockName]]] = None
):
"""Places <block> at <position>, ignoring self.transform.\n
If <position> is iterable (e.g. a list), <block> is placed at all positions.
Expand All @@ -418,24 +481,24 @@ def placeBlockGlobal(

oldBuffering = self.buffering
self.buffering = True
success = eagerAll(self._placeSingleBlockGlobal(ivec3(*pos), block, replace) for pos in position)
success = eagerAll(self._placeSingleBlockGlobal(Vec3iLike(*pos), block, replace) for pos in position)
self.buffering = oldBuffering
return success


def _placeSingleBlockGlobal(
self,
position: ivec3,
position: Vec3iLike,
block: Union[Block, Sequence[Block]],
replace: Optional[Union[str, Iterable[str]]] = None
replace: Optional[Union[BlockName, Iterable[BlockName]]] = None
):
"""Places <block> at <position>, ignoring self.transform.\n
If <block> is a sequence (e.g. a list), blocks are sampled randomly.\n
Returns whether the placement succeeded fully."""

# Check replace condition
if replace is not None:
if isinstance(replace, str):
if isinstance(replace, BlockName):
replace = [replace]
if self.getBlockGlobal(position).id not in replace:
return True
Expand Down Expand Up @@ -463,7 +526,7 @@ def _placeSingleBlockGlobal(
return True


def _placeSingleBlockGlobalDirect(self, position: ivec3, block: Block):
def _placeSingleBlockGlobalDirect(self, position: Vec3iLike, block: Block):
"""Place a single block in the world directly.\n
Returns whether the placement succeeded."""
result = interface.placeBlocks([(position, block)], dimension=self.dimension, doBlockUpdates=self.doBlockUpdates, spawnDrops=self.spawnDrops, retries=self.retries, timeout=self.timeout, host=self.host)
Expand All @@ -473,7 +536,7 @@ def _placeSingleBlockGlobalDirect(self, position: ivec3, block: Block):
return True


def _placeSingleBlockGlobalBuffered(self, position: ivec3, block: Block):
def _placeSingleBlockGlobalBuffered(self, position: Vec3iLike, block: Block):
"""Place a block in the buffer and send once limit is exceeded.\n
Returns whether placement succeeded."""
if len(self._buffer) >= self.bufferLimit:
Expand All @@ -488,7 +551,7 @@ def flushBuffer(self):
If multithreaded buffer flushing is enabled, the worker threads can be awaited with
awaitBufferFlushes()."""

def flush(blockBuffer: Dict[ivec3, Block], commandBuffer: List[str]):
def flush(blockBuffer: Dict[Vec3iLike, Block], commandBuffer: List[str]):
# Flush block buffer
if blockBuffer:
response = interface.placeBlocks(blockBuffer.items(), dimension=self.dimension, doBlockUpdates=self._bufferDoBlockUpdates, spawnDrops=self.spawnDrops, retries=self.retries, timeout=self.timeout, host=self.host)
Expand Down Expand Up @@ -594,3 +657,9 @@ def pushTransform(self, transformLike: Optional[TransformLike] = None):
yield
finally:
self.transform = originalTransform

def toBox(self):
return Box(self.offset, self.size)

def toModel(self):
return self.getBlocks(Box(ZERO_3D, self.size))
6 changes: 4 additions & 2 deletions src/gdpc/lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,13 +290,15 @@ def variate(
# soils
SPREADING_DIRTS = {"minecraft:mycelium", "minecraft:grass_block", }
DIRTS = {"minecraft:coarse_dirt", "minecraft:dirt",
"minecraft:grass_path", "minecraft:farmland", "minecraft:podzol", } \
"minecraft:farmland", "minecraft:podzol", } \
| SPREADING_DIRTS
FERTILE_SOILS = DIRTS | {"minecraft:rooted_dirt", "minecraft:moss_block", "minecraft:mud", "minecraft:muddy_mangrove_roots"}
SANDS = variate(SAND_TYPES, "sand")
GRANULARS = {"minecraft:gravel", } | SANDS
RIVERBED_SOILS = {"minecraft:dirt", "minecraft:clay",
"minecraft:sand", "minecraft:gravel", }
OVERWORLD_SOILS = DIRTS | GRANULARS | RIVERBED_SOILS
OVERWORLD_SOILS = FERTILE_SOILS | GRANULARS | RIVERBED_SOILS


NYLIUMS = variate(FUNGUS_TYPES, "nylium")
NETHERRACKS = {"minecraft:netherrack", } | NYLIUMS | NETHERRACK_ORES
Expand Down
19 changes: 10 additions & 9 deletions src/gdpc/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@

from .vector_tools import Vec3iLike, Box
from .transform import TransformLike
from .editor import Editor
from .editor import BlockGetterMixin, BlockPlacerMixin
from .block import Block


class Model:
class Model(BlockGetterMixin, BlockPlacerMixin):
"""A 3D model of Minecraft blocks.

Can be used to store a structure in memory, allowing it to be built under different
Expand All @@ -22,13 +22,14 @@ def __init__(self, size: Vec3iLike, blocks: Optional[List[Optional[Block]]] = No
"""Constructs a Model of size [size], optionally filled with [blocks]."""
self._size = ivec3(*size)
volume = self._size.x * self._size.y * self._size.z
if blocks is not None:
if len(blocks) != volume:
raise ValueError("The number of blocks should be equal to size[0] * size[1] * size[2]")
self._blocks = copy(blocks)
else:
if blocks is None:
self._blocks = [None] * volume

elif len(blocks) != volume:
raise ValueError("The number of blocks should be equal to size[0] * size[1] * size[2]")
else:
self._blocks = copy(blocks)


@property
def size(self):
Expand All @@ -45,14 +46,14 @@ def getBlock(self, position: Vec3iLike):
"""Returns the block at [vec]"""
return self._blocks[(position[0] * self._size.y + position[1]) * self._size.z + position[2]]

def setBlock(self, position: Vec3iLike, block: Optional[Block]):
def placeBlock(self, position: Vec3iLike, block: Optional[Block]):
"""Sets the block at [vec] to [block]"""
self._blocks[(position[0] * self._size.y + position[1]) * self._size.z + position[2]] = block


def build(
self,
editor: Editor,
editor: BlockPlacerMixin,
transformLike: Optional[TransformLike] = None,
substitutions: Optional[Dict[str, str]] = None,
replace: Optional[Union[str, List[str]]] = None
Expand Down
13 changes: 8 additions & 5 deletions src/gdpc/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,15 @@ def rotatedBoxTransform(box: Box, rotation: int):
"""Returns a transform that maps the box ((0,0,0), size) to [box] under [rotation], where
size == vector_tools.rotateSize3D([box].size, [rotation])."""
return Transform(
translation = box.offset + ivec3(
box.size.x - 1 if rotation in [1, 2] else 0,
0,
box.size.z - 1 if rotation in [2, 3] else 0,
translation=(
box.offset
+ ivec3(
box.size.x - 1 if rotation in {1, 2} else 0,
0,
box.size.z - 1 if rotation in {2, 3} else 0,
)
),
rotation = rotation
rotation=rotation,
)


Expand Down
Loading