Skip to content

Instantly share code, notes, and snippets.

@MinekPo1
Created January 5, 2022 14:06
Show Gist options
  • Save MinekPo1/d929f0ed1a3436054c2d62c33cf987a1 to your computer and use it in GitHub Desktop.
Save MinekPo1/d929f0ed1a3436054c2d62c33cf987a1 to your computer and use it in GitHub Desktop.
Script converting meteor waypoints to xaeros or the other way if you want for some reason.
from typing import Callable, NewType, TypeAlias, TypedDict, overload
import pynbt as nbt
from glob import glob
import os.path
from os import mkdir, name as os_name, chdir
import re
XAEROS_HEADER = """
#
#waypoint:name:initials:x:y:z:color:disabled:type:set:rotate_on_tp:tp_yaw:global
#
""".strip()
XAEROS_SETTINGS = """
//waypoints config options
usingMultiworldDetection:false
ignoreServerLevelId:false
defaultMultiworldId:mw1,1,-3
teleportationEnabled:true
usingDefaultTeleportCommand:true
sortType:NONE
sortReversed:false
//other config options
ignoreHeightmaps:false
""".strip()
Dimension = NewType("Dimension",int)
class Dimensions:
NONE = Dimension(0)
OVERWORLD = Dimension(1)
NETHER = Dimension(2)
END = Dimension(4)
DIM_NAMES = {
Dimension(0): "",
Dimension(1): "overworld",
Dimension(2): "nether",
Dimension(4): "end",
}
XColors = [
(255, 255, 255),
(255, 128, 0),
(255, 0, 255),
(128, 128, 255),
(255, 255, 0),
(128, 255, 0),
(255, 128, 255),
(128, 128, 128),
(192, 192, 192),
(0, 255, 255),
(255, 0, 255),
(0, 0, 255),
(128, 128, 0),
(0, 255, 0),
(255, 0, 0),
(0, 0, 0)
] # I have no idea if this is accurate
color_tuple: TypeAlias = tuple[int,int,int,bool] | tuple[int,int,int,int] | tuple[int,int,int] # noqa: E501
def color_tuple_to_XColor(c: color_tuple) -> int:
if len(c) != 3:
c = c[:3]
min_d = float("inf")
min_x = -1
for x,t in enumerate(XColors):
d = sum((t[i]-c[i])**2 for i in range(3))
if d < min_d:
min_d = d
min_x = x
return min_x
class MeteorColor(TypedDict):
r: nbt.TAG_Int
g: nbt.TAG_Int
b: nbt.TAG_Int
a: nbt.TAG_Int
rainbow: nbt.TAG_Byte
class MeteorWaypoint(TypedDict):
name: nbt.TAG_String
x: nbt.TAG_Int
y: nbt.TAG_Int
z: nbt.TAG_Int
color: MeteorColor
icon: nbt.TAG_String
maxVisibleDistance: nbt.TAG_Int
dimension: nbt.TAG_String
visible: nbt.TAG_Byte
scale: nbt.TAG_Double
overworld: nbt.TAG_Byte
nether: nbt.TAG_Byte
end: nbt.TAG_Byte
class RB:
ipv4 = re.compile(r"([12]*\d{1,2}\.){3}[12]*\d{1,2}")
ipv6 = re.compile(r"((([0-9A-Fa-f]{1,4}):){7}[0-9A-Fa-f]{1,4}|(([0-9A-Fa-f]{1,4}):){,7}:(([0-9A-Fa-f]{1,4}):){,7}[0-9A-Fa-f]{1,4})") # noqa: E501
address = re.compile(r"([a-zA-Z]+\.)?[a-zA-Z]+\.[a-zA-Z]+")
port = re.compile(r"\d{1,5}")
ipv4_port = re.compile(r"([12]*\d{1,2}\.){3}[12]*\d{1,2}:\d{1,5}")
ipv6_port = re.compile(r"((([0-9A-Fa-f]{1,4}):){7}[0-9A-Fa-f]{1,4}|(([0-9A-Fa-f]{1,4}):){,7}:(([0-9A-Fa-f]{1,4}):){,7}[0-9A-Fa-f]{1,4}):\d{1,5}") # noqa: E501
ipv4_port_= re.compile(r"([12]*\d{1,2}\.){3}[12]*\d{1,2}_\d{1,5}")
ipv6_port_= re.compile(r"((([0-9A-Fa-f]{1,4}):){7}[0-9A-Fa-f]{1,4}|(([0-9A-Fa-f]{1,4}):){,7}:(([0-9A-Fa-f]{1,4}):){,7}[0-9A-Fa-f]{1,4})_\d{1,5}") # noqa: E501
class Waypoint(object):
color: color_tuple | None
dimension: Dimension
visible_dim: Dimension
icon: str
max_distance: int
initials: str
active: bool
set: str
tp_yaw: float | None
is_global: bool | None
world: str
is_server: bool
@overload
def __init__(self, x: int, y: int, z: int, name:str, *,
color: tuple[int,int,int] = ..., icon: str = ..., max_distance: int = ...,
initials: str = ..., active: bool = ..., set: str = ..., tp_yaw: float = ...,
is_global: bool = ..., world: str = ..., is_server: bool = ...,
visible_dim: Dimension | int = ..., scale: float = ...,
dimension: Dimension | int = ...,):
...
@overload
def __init__(self,x:int,y:int,z:int,name:str):
...
def __init__(self,x,y,z,name,**kwds):
self.x = x
self.y = y
self.z = z
self.name = name
self.color = kwds.get('color',None)
self.dimension = kwds.get('dimension',Dimensions.OVERWORLD)
self.visible_dim = kwds.get('visible_dim',self.dimension)
self.icon = kwds.get('icon','square')
self.max_distance = kwds.get('max_distance',1000)
self.initials = kwds.get('initials',name[0])
self.active = kwds.get('active',True)
self.set = kwds.get('set',"0")
self.tp_yaw = kwds.get('tp_yaw',None)
self.is_global = kwds.get('is_global',None)
self.world = kwds.get('world',None)
self.is_server = kwds.get('is_server',False)
self.scale = kwds.get('scale',1)
def __repr__(self) -> str:
return f"<Waypoint {self.name} ({self.x},{self.y},{self.z})>"
def load_meteor(path:str) -> list[Waypoint]:
out: list[Waypoint] = []
if not(os.path.exists(os.path.join(path,"meteor-client/waypoints"))):
raise Exception("Folder is not valid. "
"Make sure you are in the instances root folder.")
for i in glob(os.path.join(path,"meteor-client/waypoints/*.nbt")):
name = os.path.basename(i).replace(".nbt","")
server = False
if RB.ipv4_port_.fullmatch(name) or RB.ipv6_port_.fullmatch(name):
name = name.replace("_",":")
server = True
elif RB.ipv4.fullmatch(name) or RB.ipv6.fullmatch(name)\
or RB.address.fullmatch(name):
server = True
with open(i,"rb") as f:
data = nbt.NBTFile(f)
w: MeteorWaypoint
for w in data['waypoints']:
kwargs = {
'name': w['name'].value,
'x': w['x'].value,
'y': w['y'].value,
'z': w['z'].value,
'color': (
w['color']["r"].value,
w['color']["g"].value,
w['color']["b"].value,
w['color']["a"].value,
w['color']["rainbow"].value
),
'dimension': getattr(Dimensions,w['dimension'].value.upper()),
'icon': w['icon'].value,
'max_distance': w['maxVisibleDistance'].value,
'active': w['visible'].value,
'scale': w['scale'].value,
'set': "Meteor WPs"
}
dims = Dimensions.NONE
if w['overworld'].value:
dims |= Dimensions.OVERWORLD
if w['nether'].value:
dims |= Dimensions.NETHER
if w['end'].value:
dims |= Dimensions.END
out.append(Waypoint(world=name,is_server=server,visible_dim=dims,**kwargs))
return out
def load_xaeros(path:str) -> list[Waypoint]:
out: list[Waypoint] = []
if not(os.path.exists(os.path.join(path,"XaeroWaypoints"))):
raise Exception("Folder is not valid. "
"Make sure you are in the instances root folder.")
for i in glob(os.path.join(path,"XaeroWaypoints/*")):
if i == "backup":
continue
for j in glob(os.path.join(i,"*.txt")):
with open(j) as f:
data = f.read()
for ii in data.split("\n"):
if ii.startswith("#") or not(ii):
continue
ii = ii.split(":")
if ii[0] == "waypoint":
kwargs = {
'name': ii[1],
'initials': ii[2],
'x': int(ii[3]),
'y': int(ii[4]),
'z': int(ii[5]),
'color': XColors[int(ii[6])] if ii[6] != "false" else None,
'enabled': ii[7] == "false",
'set': ii[9],
'tp_yaw': float(ii[11]) if ii[10] != "false" else None,
'global': ii[12] == "true",
}
out.append(Waypoint(world=os.path.basename(i),**kwargs))
return out
def dump_meteor(path:str,waypoints:list[Waypoint]) -> None:
if not(os.path.exists(os.path.join(path,"meteor-client/waypoints"))):
raise Exception("Folder is not valid. "
"Make sure you are in the instances root folder.")
worlds: dict[str,list[Waypoint]] = {}
for i in waypoints:
w_name = i.world
if RB.ipv4_port.fullmatch(w_name) or RB.ipv6_port.fullmatch(w_name):
w_name = w_name.replace(":","_")
if w_name not in worlds:
worlds[w_name] = []
worlds[w_name].append(i)
for i, value in worlds.items():
with open(os.path.join(path,"meteor-client/waypoints",i+".nbt"),"wb") as f:
root = nbt.TAG_List(nbt.TAG_Compound)
data = nbt.NBTFile(name="Waypoints",value=root)
for j in value:
wp: MeteorWaypoint = nbt.TAG_Compound() # type:ignore
wp['name'] = nbt.TAG_String(j.name)
wp['x'] = nbt.TAG_Int(j.x)
wp['y'] = nbt.TAG_Int(j.y)
wp['z'] = nbt.TAG_Int(j.z)
if j.color is None:
wp['color'] = nbt.TAG_Compound( # type:ignore
{
"a": nbt.TAG_Int(255),
"r": nbt.TAG_Int(255),
"g": nbt.TAG_Int(0),
"b": nbt.TAG_Int(0),
"rainbow": nbt.TAG_Byte(0),
}
)
elif len(j.color) == 3:
wp['color'] = nbt.TAG_Compound( # type:ignore
{
"a": nbt.TAG_Int(255),
"r": nbt.TAG_Int(j.color[0]),
"g": nbt.TAG_Int(j.color[1]),
"b": nbt.TAG_Int(j.color[2]),
"rainbow": nbt.TAG_Byte(0),
}
)
elif len(j.color) == 4:
wp['color'] = nbt.TAG_Compound( # type:ignore
{
"a": nbt.TAG_Int(j.color[3]),
"r": nbt.TAG_Int(j.color[0]),
"g": nbt.TAG_Int(j.color[1]),
"b": nbt.TAG_Int(j.color[2]),
"rainbow": nbt.TAG_Byte(0),
}
)
else:
wp['color'] = nbt.TAG_Compound( # type:ignore
{
"a": nbt.TAG_Int(j.color[3]),
"r": nbt.TAG_Int(j.color[0]),
"g": nbt.TAG_Int(j.color[1]),
"b": nbt.TAG_Int(j.color[2]),
"rainbow": nbt.TAG_Byte(j.color[4]),
}
)
wp['dimension'] = nbt.TAG_String(DIM_NAMES[j.dimension].capitalize())
wp['overworld'] = nbt.TAG_Byte(j.visible_dim & Dimensions.OVERWORLD)
wp['nether'] = nbt.TAG_Byte(j.visible_dim & Dimensions.NETHER)
wp['end'] = nbt.TAG_Byte(j.visible_dim & Dimensions.END)
wp['icon'] = nbt.TAG_String(j.icon)
wp['visible'] = nbt.TAG_Byte(1 if j.active else 0)
wp['scale'] = nbt.TAG_Double(j.scale)
wp['maxVisibleDistance'] = nbt.TAG_Int(j.max_distance)
root.append(wp)
data.write(f)
def dump_xaeros(path:str,waypoints:list[Waypoint],only_main_dim: bool = False)\
-> None:
if not(os.path.exists(os.path.join(path,"XaeroWaypoints"))):
raise Exception("Folder is not valid. "
"Make sure you are in the instances root folder.")
worlds: dict[str,list[list[Waypoint]]] = {}
for i in waypoints:
w_name = i.world
if RB.ipv4_port.fullmatch(w_name) or RB.ipv6_port.fullmatch(w_name):
w_name = w_name[:w_name.find(":")]
if i.is_server:
w_name = f"Multiplayer_{w_name}"
if w_name not in worlds:
worlds[w_name] = [
[],[],[]
]
if only_main_dim:
match i.dimension:
case Dimensions.OVERWORLD:
worlds[w_name][0].append(i)
case Dimensions.NETHER:
worlds[w_name][1].append(i)
case Dimensions.END:
worlds[w_name][2].append(i)
else:
dims = []
if i.visible_dim & Dimensions.OVERWORLD:
dims.append(0)
if i.visible_dim & Dimensions.NETHER:
dims.append(1)
if i.visible_dim & Dimensions.END:
dims.append(2)
for j in dims:
worlds[w_name][j].append(i)
for i,j in worlds.items():
if not(os.path.exists(os.path.join(path,"XaeroWaypoints",i))):
mkdir(os.path.join(path,"XaeroWaypoints",i))
with open(os.path.join(path,"XaeroWaypoints",i,"config.txt"),'w') as f:
f.write(f"{XAEROS_SETTINGS}\n")
w_path = os.path.join(path,"XaeroWaypoints",i)
for i2,j2 in enumerate(j):
sets = "sets:gui.xaero_default:"
main = ""
di = [0,-1,1][i2]
if os.path.exists(os.path.join(w_path,f"dim%{di}","mw$default_1.txt")):
with open(os.path.join(w_path,f"dim%{di}","mw$default_1.txt"),'r') as f:
for line in f.readlines():
if line.startswith("sets:"):
for s in line.split(":")[1:]:
if s.endswith("\n"):
s = s[:-1]
if s == "gui.xaero_default":
continue
if f":{s}:" not in sets:
sets += f"{s}:"
if line.startswith("waypoints:"):
main += line
if not(os.path.exists(os.path.join(w_path,f"dim%{di}"))):
mkdir(os.path.join(w_path,f"dim%{di}"))
for wp in j2:
if f":{wp.set}:" not in sets:
sets += f"{wp.set}:"
color = wp.color
if color is None:
color = "false"
else:
color = color_tuple_to_XColor(color)
disabled = "false" if wp.active else "true"
do_rot = "true" if wp.tp_yaw is not None else "false"
rot = wp.tp_yaw if wp.tp_yaw is not None else 0
gl = "true" if wp.is_global is not None else "false"
main += f"waypoint:{wp.name}:{wp.x}:{wp.y}:{wp.z}:{color}:{disabled}:0:"\
f"{wp.set}:{do_rot}:{rot}:{gl}\n"
with open(os.path.join(w_path,f"dim%{di}","mw$default_1.txt"),'w') as f:
f.write("\n".join([sets,XAEROS_HEADER,main]))
LAUNCHERS = [
# (name, win_path, linux path)
("Official Minecraft Launcher",None,None),
("GDLauncher","%appdata%/gdlauncher_next/instances",None),
("Cristall launcher","%appdata%/Crystal-Launcher/instances",None),
("MultiMC","%appdata%/MultiMC/instances",None),
]
SEEKED_FOLDERS: dict[str,tuple[str,Callable[[str],list[Waypoint]],Callable[[str,list[Waypoint]],None]]] = { # noqa: E501
# folder: (name, loader, dumper)
"meteor-client": ("meteor",load_meteor,dump_meteor),
"XaeroWaypoints": ("xaeros",load_xaeros,dump_xaeros),
}
def interface(): # sourcery no-metrics
from prompt_toolkit import prompt
from prompt_toolkit.completion import Completer, Completion, PathCompleter
from prompt_toolkit.validation import ValidationError, Validator
from prompt_toolkit.document import Document
class IsContained(Validator):
def __init__(self,elements: list[str], strict: bool = False) -> None:
super().__init__()
self.elems = elements
self.strict = strict
def validate(self, document: Document) -> None:
text = document.text
if text in self.elems:
return
if self.strict or text == "":
raise ValidationError(message="Invalid choice.",cursor_position=len(text))
for i in self.elems:
if i.startswith(text):
return
raise ValidationError(message="Invalid choice.",cursor_position=len(text))
class ListCompleter(Completer):
def __init__(self,elements: list[str]) -> None:
super().__init__()
self.elems = elements
def get_completions(self,document: Document,complete_event):
text = document.text.lower()
for i in self.elems:
if i.lower().startswith(text):
yield Completion(i,start_position=-len(document.text))
def check_directory(path: str) -> bool:
if not(os.path.exists(path) and os.path.isdir(path)):
return False
found = sum(
1 for i in SEEKED_FOLDERS.keys() if os.path.exists(os.path.join(path, i))
)
return found >= 2
CheckDirectoryValidator = Validator.from_callable(check_directory)
chdir(os.path.expanduser('~'))
print(os.path.abspath("."))
print(os.path.expanduser("~"))
print("""
Welcome to the interface.
Select your launcher:""")
for i,j in enumerate(LAUNCHERS):
print(f"\t{i+1}: {j[0]}")
print(f"\t{len(LAUNCHERS) + 1}: Custom path")
launcher = ""
while not(
launcher.isdigit() and int(launcher)-1 in range(len(LAUNCHERS) + 1)):
launcher = input(">: ")
launcher = int(launcher)-1
if launcher == 0:
r = ""
while r.lower() not in ["y","n","yes","no","1","0"]:
r = input("use default directory? (y/n):")
if os_name == "nt":
ins_dir = os.path.join(os.path.expandvars("%appdata%"),'.minecraft')
else:
ins_dir = os.path.join(os.path.expandvars("$HOME"),'.minecraft')
if r.lower() in ["n","no","0"]:
try:
ins_dir = prompt("Enter the path to the instance: ",
completer=PathCompleter(only_directories=True,expanduser=True),
validator=CheckDirectoryValidator,
validate_while_typing=False,
default=ins_dir,
)
except KeyboardInterrupt:
return
else:
if launcher == len(LAUNCHERS):
try:
launch_dir = prompt("Enter the path to the directory containing "
"instances: ",
completer=PathCompleter(only_directories=True,expanduser=True),
validator=CheckDirectoryValidator,
validate_while_typing=False,
)
except KeyboardInterrupt:
return
else:
launch_dir = LAUNCHERS[launcher][1 + (os_name != "nt")]
launch_dir = os.path.expandvars(launch_dir)
if os_name == "nt":
launch_dir = os.path.expanduser(launch_dir)
if launch_dir is None or not(os.path.exists(launch_dir)):
print("\tCould not find instance directory.")
print("\tPlease select another launcher or use Custom path.")
return
found_one = []
found_two = []
for i in SEEKED_FOLDERS:
for j in glob(os.path.join(launch_dir,"*",i)):
j = os.path.split(j)[0]
j = os.path.split(j)[1]
if j in found_one:
found_one.remove(j)
found_two.append(j)
continue
if j not in found_two:
found_one.append(j)
if len(found_two) == 0:
print("\tCould not find any instances.")
print("\tPlease select another launcher or use Custom path.")
return
elif len(found_two) < 10:
print("\tFound the following instances:")
for i in found_two:
print(f"\t{i}")
else:
print(f"found {len(found_two)} valid instances")
try:
p = prompt(
"Instance:",completer=ListCompleter(found_two),
validator=IsContained(found_two),
validate_while_typing=False,complete_while_typing=True
)
except KeyboardInterrupt:
return
if p in found_two:
ins_dir = os.path.join(launch_dir,p)
else:
for i in found_two:
if p.startswith(i):
ins_dir = i
break
else:
print("\tCould not find instance directory.")
print("\tPlease select another launcher or use Custom path.")
return
available: dict[str,tuple[Callable[[str],list[Waypoint]],Callable[[str,list[Waypoint]],None]]] = {} # noqa: E501
names = []
for i,e in SEEKED_FOLDERS.items():
if os.path.exists(os.path.join(ins_dir,i)):
available[e[0]] = (e[1:])
names.append(e[0])
try:
start = prompt(
"Convert from: ",completer=ListCompleter(names),
validator=IsContained(names),
validate_while_typing=False,complete_while_typing=True
)
names.remove(start)
end = prompt(
"Convert to: ",completer=ListCompleter(names),
validator=IsContained(names),
validate_while_typing=False,complete_while_typing=True
)
except KeyboardInterrupt:
return
available[end][1](ins_dir,available[start][0](ins_dir))
print("done!")
# add a slash to the end to avoid being piceked up by wildcards
HELP_TEXT: dict[tuple[()] | tuple[str,...],str] = {
(): "Use the help command for usage",
("help",): "Commands:\n$(help,*)",
("help","help"): " help | Shows help for a command",
("help","interface"): " interface | Launches the interface. alias: i",
("help","convert"): " convert | Converts a minecraft instance. alias: c",
("help","i","/"): "$(help,interface)",
("help","c","/"): "$(help,convert)",
("convert",): "Converts a minecraft instance.\n$(convert,usage)",
("convert","usage"): "Usage: convert <instance path> <intake format> <output format>", # noqa: E501
("c",): "$(convert,)",
("c","usage"): "$(convert,usage)",
}
def get_help_text(args: list[str]):
if "*" not in args:
while True:
if tuple(args) in HELP_TEXT:
break
if tuple(args)+("/",) in HELP_TEXT:
args.append("/")
break
args.pop()
return HELP_TEXT[tuple(args)]
else:
matches = []
for i in HELP_TEXT:
if len(args) != len(i):
continue
for a,h in zip(args,i):
if a == "*":
continue
if a != h:
break
else:
matches.append(HELP_TEXT[i])
return "\n".join(matches)
if __name__ == "__main__":
import sys
match sys.argv[1:]:
case ["i" | "interface"]:
interface()
case ['convert' | 'c',str(path),str(start),str(to)]:
if not(os.path.exists(path)):
print("Directory not found.")
sys.exit(1)
wps = None
dump = None
for k,i in SEEKED_FOLDERS.items():
if i[0] == start:
if not(os.path.exists(os.path.join(path,i[0]))):
print(f"Invalid folder ({k} not found).")
sys.exit(1)
wps = i[1](path)
if i[0] == to:
if not(os.path.exists(os.path.join(path,i[0]))):
print(f"Invalid folder ({k} not found).")
sys.exit(1)
dump = i[2]
if wps is None:
print(f"Invalid format: {start!r}")
sys.exit(1)
if dump is None:
print(f"Invalid format: {to!r}")
sys.exit(1)
dump(path,wps)
print("done!")
case _:
text = get_help_text(sys.argv[1:])
while m:=re.search(r"\$\((.+)\)",text):
if m[1].isnumeric():
text = text.replace(m[0],sys.argv[int(m[1])+1])
else:
text = text.replace(m[0],get_help_text(m[1].split(",")))
print(text)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment