mcio-ctrl 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcio_ctrl-1.4.0/PKG-INFO +57 -0
- mcio_ctrl-1.4.0/README.md +27 -0
- mcio_ctrl-1.4.0/pyproject.toml +59 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/__about__.py +1 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/__init__.py +32 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/cbor.py +78 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/config.py +118 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/controller.py +193 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/envs/__init__.py +19 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/envs/base_env.py +317 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/envs/env_util.py +137 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/envs/mcio_env.py +144 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/envs/minerl_env.py +118 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/gui.py +302 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/instance.py +369 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/mc_mock.py +159 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/mcio_gui.py +133 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/network.py +414 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/py.typed +0 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/resources/minerl_cursor_16x16.npy +0 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/scripts/README.md +3 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/scripts/mcio_cmd.py +520 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/server.py +139 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/types.py +411 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/util.py +465 -0
- mcio_ctrl-1.4.0/src/mcio_ctrl/world.py +228 -0
mcio_ctrl-1.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcio-ctrl
|
|
3
|
+
Version: 1.4.0
|
|
4
|
+
Summary: Python interface to connect to the MCio Minecraft mod
|
|
5
|
+
Author: TwoTurtles
|
|
6
|
+
Author-email: TwoTurtles <97465192+twoturtles@users.noreply.github.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Dist: pyzmq>=26.2.0
|
|
12
|
+
Requires-Dist: cbor2>=5.6.5
|
|
13
|
+
Requires-Dist: glfw>=2.7.0
|
|
14
|
+
Requires-Dist: pyopengl>=3.1.7
|
|
15
|
+
Requires-Dist: gymnasium>=1.0.0
|
|
16
|
+
Requires-Dist: pillow>=11.0.0
|
|
17
|
+
Requires-Dist: imageio>=2.37.0
|
|
18
|
+
Requires-Dist: imageio-ffmpeg>=0.6.0
|
|
19
|
+
Requires-Dist: minecraft-launcher-lib>=7.1
|
|
20
|
+
Requires-Dist: tqdm>=4.67.1
|
|
21
|
+
Requires-Dist: requests>=2.32.3
|
|
22
|
+
Requires-Dist: ruamel-yaml>=0.18.6
|
|
23
|
+
Requires-Dist: dacite>=1.8.1
|
|
24
|
+
Requires-Dist: nbt>=1.5.1
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Project-URL: Changelog, https://github.com/twoturtles/mcio_ctrl/blob/main/CHANGELOG.md
|
|
27
|
+
Project-URL: Issues, https://github.com/twoturtles/mcio_ctrl/issues
|
|
28
|
+
Project-URL: Source, https://github.com/twoturtles/mcio_ctrl
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# mcio_ctrl
|
|
32
|
+
|
|
33
|
+
### [MCio mod](https://github.com/twoturtles/MCio) | [mcio_ctrl](https://github.com/twoturtles/mcio_ctrl) | [Documentation](https://github.com/twoturtles/mcio_ctrl/wiki) | [Discord](https://discord.gg/PBfdc27h4q)
|
|
34
|
+
|
|
35
|
+
`mcio_ctrl` is a comprehensive Python library designed to interface seamlessly with [MCio](https://github.com/twoturtles/MCio), a Minecraft Fabric mod tailored for AI agent development. It includes a versatile [Gymnasium](https://gymnasium.farama.org/) environment, making it ideal for reinforcement learning research and development.
|
|
36
|
+
|
|
37
|
+
## Key Features
|
|
38
|
+
|
|
39
|
+
* **Simplified Installation and Launching:** API and commands to easily install Minecraft with the MCio mod, create custom worlds, and launch the game directly from Python.
|
|
40
|
+
* **Pre-built Gymnasium Environments:** Offers example environments, including compatibility with MineRL 1.0 actions and observations, all leveraging the robust low-level API.
|
|
41
|
+
* **Customizable Base Environment:** A convenient base class for quickly creating tailored Gymnasium environments suited to specific research needs.
|
|
42
|
+
* **Interactive GUI Support:** Enables human control of Minecraft through the standard Minecraft controls using the MCio backend. (Seamless Human-in-the-loop is planned for future updates.)
|
|
43
|
+
* **Type-Hinting and Development Convenience:** Fully type-hinted for easier integration, improved code clarity, and streamlined development workflows.
|
|
44
|
+
* **BONUS:** Easily [set up VPT and STEVE-1](https://github.com/jxiong21029/mcio-vpt-example) on modern Minecraft with support for [Sodium](https://modrinth.com/mod/sodium)!
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## Quick Links
|
|
48
|
+
|
|
49
|
+
* **Documentation:** Find comprehensive documentation and tutorials on our [Wiki](https://github.com/twoturtles/mcio_ctrl/wiki).
|
|
50
|
+
|
|
51
|
+
* **MCio Mod:**
|
|
52
|
+
* [GitHub Repository](https://github.com/twoturtles/MCio)
|
|
53
|
+
* [Modrinth Project Page](https://modrinth.com/mod/mcio)
|
|
54
|
+
|
|
55
|
+
* **Python Interface (`mcio_ctrl`):**
|
|
56
|
+
* [GitHub Repository](https://github.com/twoturtles/mcio_ctrl)
|
|
57
|
+
* [PyPI Package](https://pypi.org/project/mcio_ctrl/)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# mcio_ctrl
|
|
2
|
+
|
|
3
|
+
### [MCio mod](https://github.com/twoturtles/MCio) | [mcio_ctrl](https://github.com/twoturtles/mcio_ctrl) | [Documentation](https://github.com/twoturtles/mcio_ctrl/wiki) | [Discord](https://discord.gg/PBfdc27h4q)
|
|
4
|
+
|
|
5
|
+
`mcio_ctrl` is a comprehensive Python library designed to interface seamlessly with [MCio](https://github.com/twoturtles/MCio), a Minecraft Fabric mod tailored for AI agent development. It includes a versatile [Gymnasium](https://gymnasium.farama.org/) environment, making it ideal for reinforcement learning research and development.
|
|
6
|
+
|
|
7
|
+
## Key Features
|
|
8
|
+
|
|
9
|
+
* **Simplified Installation and Launching:** API and commands to easily install Minecraft with the MCio mod, create custom worlds, and launch the game directly from Python.
|
|
10
|
+
* **Pre-built Gymnasium Environments:** Offers example environments, including compatibility with MineRL 1.0 actions and observations, all leveraging the robust low-level API.
|
|
11
|
+
* **Customizable Base Environment:** A convenient base class for quickly creating tailored Gymnasium environments suited to specific research needs.
|
|
12
|
+
* **Interactive GUI Support:** Enables human control of Minecraft through the standard Minecraft controls using the MCio backend. (Seamless Human-in-the-loop is planned for future updates.)
|
|
13
|
+
* **Type-Hinting and Development Convenience:** Fully type-hinted for easier integration, improved code clarity, and streamlined development workflows.
|
|
14
|
+
* **BONUS:** Easily [set up VPT and STEVE-1](https://github.com/jxiong21029/mcio-vpt-example) on modern Minecraft with support for [Sodium](https://modrinth.com/mod/sodium)!
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## Quick Links
|
|
18
|
+
|
|
19
|
+
* **Documentation:** Find comprehensive documentation and tutorials on our [Wiki](https://github.com/twoturtles/mcio_ctrl/wiki).
|
|
20
|
+
|
|
21
|
+
* **MCio Mod:**
|
|
22
|
+
* [GitHub Repository](https://github.com/twoturtles/MCio)
|
|
23
|
+
* [Modrinth Project Page](https://modrinth.com/mod/mcio)
|
|
24
|
+
|
|
25
|
+
* **Python Interface (`mcio_ctrl`):**
|
|
26
|
+
* [GitHub Repository](https://github.com/twoturtles/mcio_ctrl)
|
|
27
|
+
* [PyPI Package](https://pypi.org/project/mcio_ctrl/)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mcio_ctrl"
|
|
3
|
+
version = "1.4.0"
|
|
4
|
+
description = "Python interface to connect to the MCio Minecraft mod"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "TwoTurtles", email = "97465192+twoturtles@users.noreply.github.com" },
|
|
8
|
+
]
|
|
9
|
+
license = "MIT"
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
]
|
|
15
|
+
requires-python = ">=3.12"
|
|
16
|
+
dependencies = [
|
|
17
|
+
"pyzmq>=26.2.0",
|
|
18
|
+
"cbor2>=5.6.5",
|
|
19
|
+
"glfw>=2.7.0",
|
|
20
|
+
"PyOpenGL>=3.1.7",
|
|
21
|
+
"gymnasium>=1.0.0",
|
|
22
|
+
"pillow>=11.0.0",
|
|
23
|
+
"imageio>=2.37.0",
|
|
24
|
+
"imageio-ffmpeg>=0.6.0",
|
|
25
|
+
"minecraft-launcher-lib>=7.1",
|
|
26
|
+
"tqdm>=4.67.1",
|
|
27
|
+
"requests>=2.32.3",
|
|
28
|
+
"ruamel.yaml>=0.18.6",
|
|
29
|
+
"dacite>=1.8.1",
|
|
30
|
+
"NBT>=1.5.1",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["uv_build>=0.7.12,<0.8"]
|
|
35
|
+
build-backend = "uv_build"
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Source = "https://github.com/twoturtles/mcio_ctrl"
|
|
39
|
+
Issues = "https://github.com/twoturtles/mcio_ctrl/issues"
|
|
40
|
+
Changelog = "https://github.com/twoturtles/mcio_ctrl/blob/main/CHANGELOG.md"
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
mcio = "mcio_ctrl.scripts.mcio_cmd:main"
|
|
44
|
+
|
|
45
|
+
[dependency-groups]
|
|
46
|
+
dev = [
|
|
47
|
+
"pre-commit>=4.0.1",
|
|
48
|
+
"mypy>=1.13.0",
|
|
49
|
+
"ruff>=0.8.1",
|
|
50
|
+
"black>=24.10.0",
|
|
51
|
+
"pip",
|
|
52
|
+
"pytest>=8.3.4",
|
|
53
|
+
"pytest-mock>=3.14.0",
|
|
54
|
+
"pytest-cov>=6.0.0",
|
|
55
|
+
"isort>=5.13.2",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.isort]
|
|
59
|
+
profile = "black"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.3.1.dev0"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from . import (
|
|
2
|
+
config,
|
|
3
|
+
controller,
|
|
4
|
+
envs,
|
|
5
|
+
gui,
|
|
6
|
+
instance,
|
|
7
|
+
mc_mock,
|
|
8
|
+
mcio_gui,
|
|
9
|
+
network,
|
|
10
|
+
server,
|
|
11
|
+
types,
|
|
12
|
+
util,
|
|
13
|
+
world,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__version__ = "1.4.0"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"__version__",
|
|
20
|
+
"config",
|
|
21
|
+
"controller",
|
|
22
|
+
"envs",
|
|
23
|
+
"gui",
|
|
24
|
+
"instance",
|
|
25
|
+
"mc_mock",
|
|
26
|
+
"mcio_gui",
|
|
27
|
+
"network",
|
|
28
|
+
"server",
|
|
29
|
+
"types",
|
|
30
|
+
"util",
|
|
31
|
+
"world",
|
|
32
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""CBOR processing helpers"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import fields, is_dataclass
|
|
5
|
+
from typing import Any, Final, TypeVar
|
|
6
|
+
|
|
7
|
+
import cbor2
|
|
8
|
+
|
|
9
|
+
LOG = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
# Used by MCio and mcio_ctrl to annotate protocol classes
|
|
12
|
+
MCIO_PROTOCOL_TYPE: Final[str] = "__mcio_type__"
|
|
13
|
+
|
|
14
|
+
# Maps protcol class names to the type and vice versa
|
|
15
|
+
_MCIO_NAME_TO_TYPE: dict[str, type] = {}
|
|
16
|
+
_MCIO_TYPE_TO_NAME: dict[type, str] = {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
T = TypeVar("T")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def MCioType(cls: type[T]) -> type[T]:
|
|
23
|
+
"""Decorator to register a class as used in the MCio protocol"""
|
|
24
|
+
# Use the Java Jackson style MINIMAL_CLASS name which includes a leading dot.
|
|
25
|
+
name = "." + cls.__name__
|
|
26
|
+
_MCIO_NAME_TO_TYPE[name] = cls
|
|
27
|
+
_MCIO_TYPE_TO_NAME[cls] = name
|
|
28
|
+
return cls
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def encode(obj: Any) -> bytes:
|
|
32
|
+
"""Encode object to CBOR using MCioType class annotations"""
|
|
33
|
+
return cbor2.dumps(typed_asdict(obj))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def decode(data: bytes) -> Any | None:
|
|
37
|
+
"""Decode CBOR using MCioType classes where possible. Returns None on error"""
|
|
38
|
+
try:
|
|
39
|
+
return cbor2.loads(data, object_hook=_object_hook)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
LOG.error(f"CBOR load error: {type(e).__name__}: {e}")
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _object_hook(decoder: cbor2.CBORDecoder, obj_dict: dict[Any, Any]) -> Any:
|
|
46
|
+
"""Used by the CBOR parser. Decodes packet entries into MCioType classes where possible."""
|
|
47
|
+
mcio_type = obj_dict.pop(MCIO_PROTOCOL_TYPE, None)
|
|
48
|
+
if isinstance(mcio_type, str):
|
|
49
|
+
cls = _MCIO_NAME_TO_TYPE.get(mcio_type)
|
|
50
|
+
if cls:
|
|
51
|
+
return cls(**obj_dict)
|
|
52
|
+
else:
|
|
53
|
+
LOG.error(f"Unknown MCioType type: {mcio_type}")
|
|
54
|
+
|
|
55
|
+
# Non MCioType
|
|
56
|
+
return obj_dict
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def typed_asdict(obj: Any) -> Any:
|
|
60
|
+
"""Like dataclass asdict, but annotates MCioType classes with type info.
|
|
61
|
+
Recursively walks the dataclass.
|
|
62
|
+
"""
|
|
63
|
+
if is_dataclass(obj):
|
|
64
|
+
cls = type(obj)
|
|
65
|
+
cls_name = _MCIO_TYPE_TO_NAME.get(cls)
|
|
66
|
+
result = {
|
|
67
|
+
key: typed_asdict(getattr(obj, key))
|
|
68
|
+
for key in (f.name for f in fields(obj))
|
|
69
|
+
}
|
|
70
|
+
if cls_name:
|
|
71
|
+
result[MCIO_PROTOCOL_TYPE] = cls_name
|
|
72
|
+
return result
|
|
73
|
+
elif isinstance(obj, (list, tuple)):
|
|
74
|
+
return [typed_asdict(i) for i in obj]
|
|
75
|
+
elif isinstance(obj, dict):
|
|
76
|
+
return {k: typed_asdict(v) for k, v in obj.items()}
|
|
77
|
+
else:
|
|
78
|
+
return obj
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Persistent config - mcio.yaml"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import types
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Final, Optional, TypeAlias
|
|
9
|
+
|
|
10
|
+
import dacite
|
|
11
|
+
from ruamel.yaml import YAML
|
|
12
|
+
|
|
13
|
+
LOG = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
# Global defines
|
|
17
|
+
|
|
18
|
+
DEFAULT_MCIO_DIR: Final[Path] = Path("~/.mcio/").expanduser()
|
|
19
|
+
DEFAULT_MINECRAFT_VERSION: Final[str] = "1.21.3"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
##
|
|
23
|
+
# Configuration
|
|
24
|
+
|
|
25
|
+
# XXX Consider saving necessary config in each entity's directory
|
|
26
|
+
|
|
27
|
+
CONFIG_FILENAME: Final[str] = "mcio.yaml"
|
|
28
|
+
CONFIG_VERSION: Final[int] = 1
|
|
29
|
+
InstanceName: TypeAlias = str
|
|
30
|
+
WorldName: TypeAlias = str
|
|
31
|
+
MinecraftVersion: TypeAlias = str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class WorldConfig:
|
|
36
|
+
name: WorldName = ""
|
|
37
|
+
minecraft_version: MinecraftVersion = "" # Save the version that created this world
|
|
38
|
+
seed: str = ""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class InstanceConfig:
|
|
43
|
+
name: InstanceName = ""
|
|
44
|
+
launch_version: MinecraftVersion = ""
|
|
45
|
+
minecraft_version: MinecraftVersion = ""
|
|
46
|
+
worlds: dict[WorldName, WorldConfig] = field(default_factory=dict)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class ServerConfig:
|
|
51
|
+
minecraft_version: MinecraftVersion = ""
|
|
52
|
+
jvm_version: str = ""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Config:
|
|
57
|
+
config_version: int = CONFIG_VERSION # XXX Eventually check this
|
|
58
|
+
instances: dict[InstanceName, InstanceConfig] = field(default_factory=dict)
|
|
59
|
+
world_storage: dict[WorldName, WorldConfig] = field(default_factory=dict)
|
|
60
|
+
servers: dict[MinecraftVersion, ServerConfig] = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_dict(cls, config_dict: dict[str, Any]) -> Optional["Config"]:
|
|
64
|
+
try:
|
|
65
|
+
rv = dacite.from_dict(data_class=cls, data=config_dict)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
# This means the dict doesn't match Config
|
|
68
|
+
LOG.error(f"Failed to parse config file: {e}")
|
|
69
|
+
return None
|
|
70
|
+
return rv
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> dict[str, Any]:
|
|
73
|
+
return asdict(self)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ConfigManager:
|
|
77
|
+
def __init__(self, mcio_dir: Path | str, save: bool = False) -> None:
|
|
78
|
+
"""Set save to true to save automatically on exiting"""
|
|
79
|
+
self.save_on_exit = save
|
|
80
|
+
mcio_dir = Path(mcio_dir).expanduser()
|
|
81
|
+
self.config_file = mcio_dir / CONFIG_FILENAME
|
|
82
|
+
self.yaml = YAML(typ="rt")
|
|
83
|
+
self.config: Config = Config()
|
|
84
|
+
|
|
85
|
+
def load(self) -> None:
|
|
86
|
+
if self.config_file.exists():
|
|
87
|
+
with open(self.config_file) as f:
|
|
88
|
+
# load() returns None if the file has no data.
|
|
89
|
+
cfg_dict = self.yaml.load(f) or {}
|
|
90
|
+
self.config = Config.from_dict(cfg_dict) or Config()
|
|
91
|
+
else:
|
|
92
|
+
self.config = Config()
|
|
93
|
+
|
|
94
|
+
def pformat(self) -> str:
|
|
95
|
+
"""Pretty print the config"""
|
|
96
|
+
string_stream = StringIO()
|
|
97
|
+
self.yaml.dump(self.config.to_dict(), string_stream)
|
|
98
|
+
return string_stream.getvalue()
|
|
99
|
+
|
|
100
|
+
def save(self) -> None:
|
|
101
|
+
with open(self.config_file, "w") as f:
|
|
102
|
+
self.yaml.dump(self.config.to_dict(), f)
|
|
103
|
+
|
|
104
|
+
def __enter__(self) -> "ConfigManager":
|
|
105
|
+
self.load()
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
def __exit__(
|
|
109
|
+
self,
|
|
110
|
+
exc_type: type[BaseException] | None,
|
|
111
|
+
exc_value: BaseException | None,
|
|
112
|
+
traceback: types.TracebackType | None,
|
|
113
|
+
) -> bool | None:
|
|
114
|
+
if exc_type is None:
|
|
115
|
+
# Clean exit
|
|
116
|
+
if self.save_on_exit:
|
|
117
|
+
self.save()
|
|
118
|
+
return None
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from . import network, types, util
|
|
6
|
+
|
|
7
|
+
LOG = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ControllerCommon(Protocol):
|
|
11
|
+
"""Protocol for the fundamental controller interface shared by sync/async implementations"""
|
|
12
|
+
|
|
13
|
+
_action_sequence_last_sent: int
|
|
14
|
+
_mcio_conn: network._Connection
|
|
15
|
+
|
|
16
|
+
def send_action(self, action: network.ActionPacket) -> None:
|
|
17
|
+
"""Send action to minecraft. Automatically sets action.sequence."""
|
|
18
|
+
self._action_sequence_last_sent += 1
|
|
19
|
+
action.sequence = self._action_sequence_last_sent
|
|
20
|
+
self._mcio_conn.send_action(action)
|
|
21
|
+
|
|
22
|
+
def send_stop(self) -> None:
|
|
23
|
+
"""Send a stop packet to Minecraft. This should cause Minecraft to cleanly exit."""
|
|
24
|
+
self._mcio_conn.send_stop()
|
|
25
|
+
|
|
26
|
+
def recv_observation(self) -> network.ObservationPacket: ...
|
|
27
|
+
def close(self) -> None: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ControllerSync(ControllerCommon):
|
|
31
|
+
"""
|
|
32
|
+
Handles SYNC mode connections to Minecraft.
|
|
33
|
+
Blocks in recv waiting for a new observation.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
*,
|
|
39
|
+
action_port: int | None = None,
|
|
40
|
+
observation_port: int | None = None,
|
|
41
|
+
wait_for_connection: bool = True,
|
|
42
|
+
connection_timeout: float | None = None,
|
|
43
|
+
):
|
|
44
|
+
self._action_sequence_last_sent = 0
|
|
45
|
+
self._mcio_conn = network._Connection(
|
|
46
|
+
action_port=action_port,
|
|
47
|
+
observation_port=observation_port,
|
|
48
|
+
wait_for_connection=wait_for_connection,
|
|
49
|
+
connection_timeout=connection_timeout,
|
|
50
|
+
)
|
|
51
|
+
self.check_mode = True
|
|
52
|
+
|
|
53
|
+
def recv_observation(
|
|
54
|
+
self, block: bool = True, timeout: float | None = None
|
|
55
|
+
) -> network.ObservationPacket:
|
|
56
|
+
"""Receive observation. Always blocks - ignores block and timeout args"""
|
|
57
|
+
obs = self._mcio_conn.recv_observation(block=True)
|
|
58
|
+
if obs is None:
|
|
59
|
+
# Exiting or packet decode error
|
|
60
|
+
return network.ObservationPacket()
|
|
61
|
+
|
|
62
|
+
if self.check_mode:
|
|
63
|
+
self.check_mode = False
|
|
64
|
+
mode = types.MCioMode.SYNC
|
|
65
|
+
if mode != obs.mode:
|
|
66
|
+
LOG.warning(f"Mode-Mismatch controller={mode} mcio={obs.mode}")
|
|
67
|
+
|
|
68
|
+
return obs
|
|
69
|
+
|
|
70
|
+
def close(self) -> None:
|
|
71
|
+
"""Shut down the network connection"""
|
|
72
|
+
self._mcio_conn.close()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ControllerAsync(ControllerCommon):
|
|
76
|
+
"""
|
|
77
|
+
Handles ASYNC mode connections to Minecraft
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
*,
|
|
83
|
+
action_port: int | None = None,
|
|
84
|
+
observation_port: int | None = None,
|
|
85
|
+
wait_for_connection: bool = True,
|
|
86
|
+
connection_timeout: float | None = None,
|
|
87
|
+
):
|
|
88
|
+
self._action_sequence_last_sent = 0
|
|
89
|
+
|
|
90
|
+
self.process_counter = util.TrackPerSecond("ProcessObservationPPS")
|
|
91
|
+
self.queued_counter = util.TrackPerSecond("QueuedActionsPPS")
|
|
92
|
+
self.check_mode = True
|
|
93
|
+
|
|
94
|
+
# Flag to signal observation thread to stop.
|
|
95
|
+
self._running = threading.Event()
|
|
96
|
+
self._running.set()
|
|
97
|
+
|
|
98
|
+
self._observation_queue = util.LatestItemQueue[network.ObservationPacket]()
|
|
99
|
+
self._mcio_conn = network._Connection(
|
|
100
|
+
action_port=action_port,
|
|
101
|
+
observation_port=observation_port,
|
|
102
|
+
wait_for_connection=wait_for_connection,
|
|
103
|
+
connection_timeout=connection_timeout,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Start observation thread
|
|
107
|
+
self._observation_thread = threading.Thread(
|
|
108
|
+
target=self._observation_thread_fn, name="ObservationThread"
|
|
109
|
+
)
|
|
110
|
+
self._observation_thread.daemon = True
|
|
111
|
+
self._observation_thread.start()
|
|
112
|
+
|
|
113
|
+
LOG.info("Controller init complete")
|
|
114
|
+
|
|
115
|
+
def recv_observation(
|
|
116
|
+
self, block: bool = True, timeout: float | None = None
|
|
117
|
+
) -> network.ObservationPacket:
|
|
118
|
+
"""
|
|
119
|
+
Returns the most recently received observation pulling it from the processing queue.
|
|
120
|
+
Block and timeout are like queue.Queue.get().
|
|
121
|
+
Can raise Empty exception if non-blocking or timeout is used.
|
|
122
|
+
"""
|
|
123
|
+
# RECV 2
|
|
124
|
+
observation = self._observation_queue.get(block=block, timeout=timeout)
|
|
125
|
+
return observation
|
|
126
|
+
|
|
127
|
+
def send_and_recv_match(
|
|
128
|
+
self, action: network.ActionPacket, max_skip: int | None = 5
|
|
129
|
+
) -> network.ObservationPacket:
|
|
130
|
+
"""Send action to minecraft. Automatically sets action.sequence.
|
|
131
|
+
This will ensure the next observation you receive came after this action.
|
|
132
|
+
Since we're running in async mode, observations may be in flight that occurred
|
|
133
|
+
before Minecraft processed this action.
|
|
134
|
+
max_skip is the maximum number of observations to skip before giving up.
|
|
135
|
+
Because the observations are stored in a LatestItemQueue, there shouldn't be
|
|
136
|
+
many to skip - only observations that were in flight after the action was sent, but
|
|
137
|
+
before Minecraft processed it. Generally we should only hit max_skip if something went
|
|
138
|
+
wrong, like the action was dropped. This could happen, for example, when the agent
|
|
139
|
+
starts before Minecraft.
|
|
140
|
+
"""
|
|
141
|
+
self.send_action(action)
|
|
142
|
+
wait_seq = self._action_sequence_last_sent
|
|
143
|
+
n_skip = 0
|
|
144
|
+
while True:
|
|
145
|
+
observation = self._observation_queue.get()
|
|
146
|
+
obs_action_seq = observation.last_action_sequence
|
|
147
|
+
if obs_action_seq >= wait_seq:
|
|
148
|
+
break
|
|
149
|
+
n_skip += 1
|
|
150
|
+
if max_skip is not None and n_skip >= max_skip:
|
|
151
|
+
LOG.warning("Max-Skip")
|
|
152
|
+
break
|
|
153
|
+
LOG.debug(
|
|
154
|
+
f"SKIPPING obs={observation.sequence} last_action={obs_action_seq} < waiting={wait_seq}"
|
|
155
|
+
)
|
|
156
|
+
# print(f"SKIPPING obs={observation.sequence} last_action={obs_action_seq} < waiting={wait_seq}")
|
|
157
|
+
return observation
|
|
158
|
+
|
|
159
|
+
def _observation_thread_fn(self) -> None:
|
|
160
|
+
"""Loops. Receives observation packets from minecraft and places on observation_queue"""
|
|
161
|
+
LOG.info("ObservationThread start")
|
|
162
|
+
while self._running.is_set():
|
|
163
|
+
# RECV 1
|
|
164
|
+
# I don't think we'll ever drop here. this is a short loop to recv the packet
|
|
165
|
+
# and put it on the queue to be processed.
|
|
166
|
+
observation = self._mcio_conn.recv_observation(block=True)
|
|
167
|
+
if observation is None:
|
|
168
|
+
continue # Exiting or packet decode error
|
|
169
|
+
|
|
170
|
+
if self.check_mode:
|
|
171
|
+
self.check_mode = False
|
|
172
|
+
mode = types.MCioMode.ASYNC
|
|
173
|
+
if mode != observation.mode:
|
|
174
|
+
LOG.warning(
|
|
175
|
+
f"Mode-Mismatch controller={mode} mcio={observation.mode}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
dropped = self._observation_queue.put(observation)
|
|
179
|
+
if dropped:
|
|
180
|
+
# This means the main (processing) thread isn't reading fast enough.
|
|
181
|
+
# The first few are always dropped, presumably as we empty the initial zmq buffer
|
|
182
|
+
# that built up during pause for "slow joiner syndrome".
|
|
183
|
+
# XXX This should not longer happen since we're using push/pull? Change log level?
|
|
184
|
+
LOG.debug("Dropped observation packet from processing queue")
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
LOG.info("ObservationThread shut down")
|
|
188
|
+
|
|
189
|
+
def close(self) -> None:
|
|
190
|
+
"""Shut down the network connection"""
|
|
191
|
+
self._running.clear()
|
|
192
|
+
self._mcio_conn.close()
|
|
193
|
+
self._observation_thread.join()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from gymnasium.envs.registration import register
|
|
2
|
+
|
|
3
|
+
from . import base_env, mcio_env, minerl_env
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"base_env",
|
|
7
|
+
"mcio_env",
|
|
8
|
+
"minerl_env",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
register(
|
|
12
|
+
id="MCio/MCioEnv-v0",
|
|
13
|
+
entry_point="mcio_ctrl.envs.mcio_env:MCioEnv",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
register(
|
|
17
|
+
id="MCio/MinerlEnv-v0",
|
|
18
|
+
entry_point="mcio_ctrl.envs.minerl_env:MinerlEnv",
|
|
19
|
+
)
|