|
|
import os.path |
|
|
import time |
|
|
import warnings |
|
|
from typing import SupportsFloat, Any, Tuple, Dict |
|
|
|
|
|
import requests |
|
|
import json |
|
|
|
|
|
import gymnasium as gym |
|
|
from gymnasium.core import ObsType |
|
|
|
|
|
import utils as U |
|
|
|
|
|
from .minecraft_launcher import MinecraftInstance |
|
|
from .process_monitor import SubprocessMonitor |
|
|
|
|
|
|
|
|
class VoyagerEnv(gym.Env): |
|
|
def __init__( |
|
|
self, |
|
|
mc_port=None, |
|
|
azure_login=None, |
|
|
server_host="http://127.0.0.1", |
|
|
server_port=3000, |
|
|
request_timeout=600, |
|
|
log_path="./logs", |
|
|
visual_server_port=-1 |
|
|
): |
|
|
if not mc_port and not azure_login: |
|
|
raise ValueError("Either mc_port or azure_login must be specified") |
|
|
if mc_port and azure_login: |
|
|
warnings.warn( |
|
|
"Both mc_port and mc_login are specified, mc_port will be ignored" |
|
|
) |
|
|
self.mc_port = mc_port |
|
|
self.visual_server_port = visual_server_port |
|
|
self.azure_login = azure_login |
|
|
self.server = f"{server_host}:{server_port}" |
|
|
self.server_port = server_port |
|
|
self.request_timeout = request_timeout |
|
|
self.log_path = log_path |
|
|
self.mineflayer = self.get_mineflayer_process(server_port) |
|
|
if azure_login: |
|
|
self.mc_instance = self.get_mc_instance() |
|
|
else: |
|
|
self.mc_instance = None |
|
|
self.has_reset = False |
|
|
self.reset_options = None |
|
|
self.connected = False |
|
|
self.server_paused = False |
|
|
|
|
|
def get_mineflayer_process(self, server_port): |
|
|
U.f_mkdir(self.log_path, "mineflayer") |
|
|
file_path = os.path.abspath(os.path.dirname(__file__)) |
|
|
return SubprocessMonitor( |
|
|
commands=[ |
|
|
"node", |
|
|
U.f_join(file_path, "mineflayer/index.js"), |
|
|
str(server_port), |
|
|
str(self.visual_server_port) |
|
|
], |
|
|
name="mineflayer", |
|
|
ready_match=r"Server started on port (\d+)", |
|
|
log_path=U.f_join(self.log_path, "mineflayer"), |
|
|
) |
|
|
|
|
|
def get_mc_instance(self): |
|
|
print("Creating Minecraft server") |
|
|
U.f_mkdir(self.log_path, "minecraft") |
|
|
return MinecraftInstance( |
|
|
**self.azure_login, |
|
|
mineflayer=self.mineflayer, |
|
|
log_path=U.f_join(self.log_path, "minecraft"), |
|
|
) |
|
|
|
|
|
def check_process(self): |
|
|
if self.mc_instance and not self.mc_instance.is_running: |
|
|
print("Starting Minecraft server") |
|
|
self.mc_instance.run() |
|
|
self.mc_port = self.mc_instance.port |
|
|
self.reset_options["port"] = self.mc_instance.port |
|
|
print(f"Server started on port {self.reset_options['port']}") |
|
|
retry = 0 |
|
|
while not self.mineflayer.is_running: |
|
|
print("Mineflayer process has exited, restarting") |
|
|
self.mineflayer.run() |
|
|
if not self.mineflayer.is_running: |
|
|
if retry > 3: |
|
|
raise RuntimeError("Mineflayer process failed to start") |
|
|
else: |
|
|
continue |
|
|
print(self.mineflayer.ready_line) |
|
|
res = requests.post( |
|
|
f"{self.server}/start", |
|
|
json=self.reset_options, |
|
|
timeout=self.request_timeout, |
|
|
) |
|
|
if res.status_code != 200: |
|
|
self.mineflayer.stop() |
|
|
raise RuntimeError( |
|
|
f"Minecraft server reply with code {res.status_code}" |
|
|
) |
|
|
return res.json() |
|
|
|
|
|
def step( |
|
|
self, |
|
|
code: str, |
|
|
programs: str = "", |
|
|
) -> Tuple[ObsType, SupportsFloat, bool, bool, Dict[str, Any]]: |
|
|
if not self.has_reset: |
|
|
raise RuntimeError("Environment has not been reset yet") |
|
|
self.check_process() |
|
|
self.unpause() |
|
|
data = { |
|
|
"code": code, |
|
|
"programs": programs, |
|
|
} |
|
|
res = requests.post( |
|
|
f"{self.server}/step", json=data, timeout=self.request_timeout |
|
|
) |
|
|
if res.status_code != 200: |
|
|
raise RuntimeError("Failed to step Minecraft server") |
|
|
returned_data = res.json() |
|
|
self.pause() |
|
|
return json.loads(returned_data) |
|
|
|
|
|
def render(self): |
|
|
raise NotImplementedError("render is not implemented") |
|
|
|
|
|
def reset( |
|
|
self, |
|
|
*, |
|
|
seed=None, |
|
|
options=None, |
|
|
) -> Tuple[ObsType, Dict[str, Any]]: |
|
|
if options is None: |
|
|
options = {} |
|
|
|
|
|
if options.get("inventory", {}) and options.get("mode", "hard") != "hard": |
|
|
raise RuntimeError("inventory can only be set when options is hard") |
|
|
|
|
|
self.reset_options = { |
|
|
"port": self.mc_port, |
|
|
"reset": options.get("mode", "peaceful"), |
|
|
"inventory": options.get("inventory", {}), |
|
|
"equipment": options.get("equipment", []), |
|
|
"spread": options.get("spread", False), |
|
|
"waitTicks": options.get("wait_ticks", 5), |
|
|
"position": options.get("position", None), |
|
|
} |
|
|
|
|
|
self.unpause() |
|
|
self.mineflayer.stop() |
|
|
time.sleep(1) |
|
|
|
|
|
returned_data = self.check_process() |
|
|
self.has_reset = True |
|
|
self.connected = True |
|
|
|
|
|
self.reset_options["reset"] = "soft" |
|
|
self.pause() |
|
|
return json.loads(returned_data) |
|
|
|
|
|
def close(self): |
|
|
self.unpause() |
|
|
if self.connected: |
|
|
res = requests.post(f"{self.server}/stop") |
|
|
if res.status_code == 200: |
|
|
self.connected = False |
|
|
if self.mc_instance: |
|
|
self.mc_instance.stop() |
|
|
self.mineflayer.stop() |
|
|
return not self.connected |
|
|
|
|
|
def pause(self): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return True |
|
|
|
|
|
def unpause(self): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return False |
|
|
|