Last active
June 6, 2019 22:16
-
-
Save ntoonio/595921fccb540680e32edc7c90a8dddc to your computer and use it in GitHub Desktop.
A python 3 script for controlling a Ultra Hardcore game in Minecraft
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
from mcrcon import MCRcon | |
import json | |
import datetime | |
import time | |
import os | |
import json | |
import math | |
import argparse | |
import random | |
import string | |
ALL_COLORS = ["aqua", "black", "blue", "dark_aqua", "dark_blue", "dark_gray", "dark_green", "dark_purple", "dark_red", "gold", "gray", "green", "light_purple", "red", "white", "yellow"] | |
PROPERTIES_SETTINGS = {"spawn-protection": "0", "pvp": "true", "enable-rcon": "true", "enable-command-block": "true"} | |
conf = { | |
"host": "localhost", | |
"start_time": None, | |
"spawn_point": { | |
"x": "0", | |
"z": "0" | |
}, | |
"worldborder": { | |
"start_shrink": "after", | |
"time": "0:01", | |
"first_size": "2000", | |
"second_size": "200", | |
"shrink_time": "0:01", | |
"cap_speed": "2.5", | |
"damage_buffer": "3", | |
"damage_per_block": "0.5", | |
"warning_distance": "10", | |
"warning_time": "15" | |
}, | |
"gamerules": { | |
"announceAdvancements": "true", | |
"spawnRadius": "0" | |
}, | |
"prop": {} | |
} | |
teams = {} | |
def setupSubparsers(parser, T): | |
if "subparsers" in T: | |
sub = parser.add_subparsers(dest=T["name"]) | |
for t in T["subparsers"]: | |
tParser = sub.add_parser(t["name"]) | |
if "arguments" in t: | |
for a in t["arguments"]: | |
name = a["name"] | |
b = a.copy() | |
del b["name"] | |
tParser.add_argument(name, **b) | |
setupSubparsers(tParser, t) | |
def runAction(args, T): | |
for t in T["subparsers"]: | |
if t["name"] == getattr(args, T["name"]): | |
if "cmd" in t: | |
a = vars(args) | |
b = [a[k] for k in a if k in [x["name"] for x in t["arguments"]]] if "arguments" in t else [] | |
keys = [x["name"] for x in t["arguments"] if x["name"].startswith("-")] | |
values = [a[k.lstrip("-")] for k in keys] | |
c = {k.lstrip("-"):v for k,v in zip(keys, values)} | |
t["cmd"](*b, **c) | |
elif "subparsers" in t: | |
runAction(args, t) | |
def parseArgTree(tree): | |
parser = argparse.ArgumentParser() | |
setupSubparsers(parser, tree) | |
args = parser.parse_args() | |
runAction(args, tree) | |
def mergeDict(d, d2): | |
for k, v in d2.items(): | |
if (k in d and isinstance(d[k], dict) and isinstance(d2[k], dict)): | |
mergeDict(d[k], d2[k]) | |
else: | |
d[k] = d2[k] | |
def getDictPath(path, d): | |
if path.find("/") >= 0: | |
split = path.split("/", 1) | |
return getDictPath(split[1], d[split[0]]) | |
else: | |
if path in d: | |
return d[path] | |
else: | |
return None | |
def getConf(path): | |
return getDictPath(path, conf) | |
def createCon(): | |
return MCRcon(conf["host"], getConf("prop/rcon.password"), port=int(getConf("prop/rcon.port"))) | |
def runCommand(cmd, mcr = None): | |
if mcr == None: | |
with createCon() as mcr: | |
return mcr.command(cmd) | |
else: | |
return mcr.command(cmd) | |
def say(s, mcr = None): | |
runCommand('/tellraw @a ["<", {"text": "UHC-controller", "color":"gold"}, "> ' + s + '"]', mcr) | |
def getBorder(mcr = None): | |
resp = runCommand("/worldborder get", mcr) | |
return int(resp.split()[5]) | |
def setBorder(size, time, mcr = None): | |
runCommand("/worldborder set " + str(size) + " " + str(time), mcr) | |
def zeroPad(s, w = 2): | |
return ("0" * (w - len(str(s)))) + str(s) | |
def timeToSeconds(t): | |
tt = datetime.datetime.strptime(t, "%H:%M").timetuple() | |
return tt.tm_hour * 60 * 60 + tt.tm_min * 60 | |
def secondsToTime(s): | |
hours = math.floor(s / (60 * 60)) | |
minutes = math.floor((s - hours * 60 * 60) / 60) | |
seconds = math.floor(s - hours * 60 * 60 - minutes * 60) | |
return zeroPad(hours) + ":" + zeroPad(minutes) + (":" + zeroPad(seconds) if seconds > 0 else "") | |
## Properties setup | |
def propertiesSetup(): | |
if os.path.exists("server.properties"): | |
props = {} | |
with open("server.properties") as f: | |
for line in f.readlines(): | |
if line.startswith("#"): continue | |
parts = line.split("=", 1) | |
props[parts[0]] = parts[1].rstrip() | |
for p in PROPERTIES_SETTINGS: | |
props[p] = PROPERTIES_SETTINGS[p] | |
props["rcon.port"] = input("Which port do you want to use for rcon? >") | |
rconPass = "".join(random.choice(string.ascii_letters + string.digits) for _ in range(30)) | |
props["rcon.password"] = rconPass | |
print("Your rcon password is " + rconPass) | |
with open("server.properties", "w") as f: | |
out = "#Minecraft server properties" | |
for p in props: | |
out += "\n" + p + "=" + props[p] | |
f.write(out) | |
print("Changed some properties") | |
else: | |
print("Couldn't find a server.properties file") | |
def gameruleSetup(): | |
with createCon() as mcr: | |
mcr.command("/gamerule naturalRegeneration false") | |
mcr.command("/worldborder center " + getConf("spawn_point/x") + " " + getConf("spawn_point/z")) | |
mcr.command("/worldborder set " + getConf("worldborder/first_size")) | |
mcr.command("/worldborder warning distance " + getConf("worldborder/warning_distance")) | |
mcr.command("/worldborder warning time " + getConf("worldborder/warning_time")) | |
mcr.command("/worldborder damage buffer " + getConf("worldborder/damage_buffer")) | |
mcr.command("/worldborder damage amount " + getConf("worldborder/damage_per_block")) | |
mcr.command("/gamerule doDaylightCycle false") | |
mcr.command("/time set day") | |
## Team setup | |
def teamsSetup(teamFileName = None): | |
print("Which teams do you want? Enter a color and then hit 'Enter'. It will be listening for names until 'end' is entered.") | |
while True: | |
a = input(">").lower() | |
if a == "end": | |
if len(teams) <= 1: | |
print("You need more than one team. Can't end now") | |
else: break | |
if a in ALL_COLORS: | |
if not a in teams: | |
teams[a] = [] | |
print("Selected '" + a + "'") | |
else: | |
del teams[a] | |
print(a + " was already selected. Unselected it!") | |
else: | |
print(a + " isn't a recognized color") | |
for t in teams: | |
while True: | |
print("Who do you wan't to be on team " + t + "? Go to next team by entering 'next'.") | |
name = input(">") | |
if name == "next": | |
break | |
elif not name in teams[t]: | |
teams[t].append(name) | |
print("Added " + name + " to team " + t) | |
else: | |
teams[t].remove(name) | |
print(name + " was already on team " + t + ". Removed that player from team " + t) | |
teamFileName = teamFileName if teamFileName is not None else "uhc_teams.json" | |
with open(teamFileName, "w") as f: | |
f.write(json.dumps(teams)) | |
## Game loop | |
def gameLoop(): | |
startTimestamp = time.mktime(datetime.datetime.strptime(getConf("start_time"), "%Y-%m-%d %H:%M").timetuple()) | |
if startTimestamp - time.time() > 0: | |
gameruleSetup() | |
with createCon() as mcr: | |
for t in teams: | |
mcr.command("/team add " + t) | |
mcr.command("/team modify " + t + " color " + t) | |
for p in teams[t]: | |
mcr.command("/team join " + t + " " + p) | |
say("Waiting for game to start at " + getConf("start_time"), mcr) | |
time.sleep(startTimestamp - time.time()) | |
with createCon() as mcr: | |
mcr.command("/advancement revoke @a everything") | |
mcr.command("/effect clear @a") | |
mcr.command("/clear @a") | |
mcr.command("/time set day") | |
mcr.command("/gamerule doDaylightCycle true") | |
# 4 is just an arbitrary number and can be what ever | |
distanceBetweenPlayers = math.ceil(int(getConf("worldborder/first_size")) / 4) | |
maxRange = int(int(getConf("worldborder/first_size")) / 2) | |
mcr.command("/spreadplayers " + getConf("spawn_point/x") + " " + getConf("spawn_point/z") + " " + str(distanceBetweenPlayers) + " " + str(maxRange) + " true @a") | |
else: | |
print("Time is after start time. Assuming something crashed.") | |
if getConf("worldborder/start_shrink") == "at": | |
execTimestamp = time.mktime(datetime.datetime.strptime(getConf("worldborder/time"), "%Y-%m-%d %H:%M").timetuple()) | |
if execTimestamp - time.time() > 0: | |
say("Waiting for the border to start shrinking at " + getConf("worldborder/time")) | |
time.sleep(execTimestamp - time.time()) | |
else: | |
print("The border should have started moving") | |
elif getConf("worldborder/start_shrink") == "after": | |
delay = timeToSeconds(getConf("worldborder/time")) | |
if startTimestamp + delay - time.time() > 0: | |
say("Waiting for the border to start shrinking after " + getConf("worldborder/time")) | |
time.sleep(startTimestamp + delay - time.time()) | |
else: | |
print("The border should have started moving") | |
with createCon() as mcr: | |
w = getBorder(mcr) | |
if w == int(getConf("worldborder/second_size")): | |
print("The world border has shrinked to it's intended size and UHC-controller's work here is done!") | |
return | |
# This compensates (compared to just using worldborder/shrink_time) in case the UHC-controller or the | |
# server is shut down during the game. It will move faster than it was supposed to but will be at the | |
# desired width at the asigned time. Instantly moving the worldborder to where it should be would be | |
# more accurat but might risk some players getting stuck on the outside. | |
moveTime = startTimestamp + delay + timeToSeconds(getConf("worldborder/shrink_time")) - int(round(time.time())) | |
s = (w - int(getConf("worldborder/second_size"))) / 2 | |
if moveTime > 0: | |
v = s / moveTime | |
say("The border is now moving from " + str(w) + " to " + getConf("worldborder/second_size") + " blocks wide in " + secondsToTime(moveTime) + " (" + str(round(v, 2)) + " blocks per second)", mcr) | |
# Safe guard so that a border wall won't move faster than a certain speed defined at worldborder/cap_speed. | |
if getConf("worldborder/cap_speed") is not False: | |
if v > float(getConf("worldborder/cap_speed")): | |
moveTime = math.floor(s / float(getConf("worldborder/cap_speed"))) | |
say("However, that's a bit fast. So the speed of a border wall will be caped at " + getConf("worldborder/cap_speed") + " blocks per second. The shrinking will now take " + secondsToTime(moveTime) + " instead") | |
else: | |
say("The border should've been at " + getConf("worldborder/second_size") + " blocks wide right now if everything had worked. The border will now shrink from " + str(w) + " + to " + getConf("worldborder/second_size") + " blocks wide at fastest speed allowed (" + getConf("worldborder/cap_speed") + " blocks per second)") | |
moveTime = math.floor(s / float(getConf("worldborder/cap_speed"))) | |
setBorder(getConf("worldborder/second_size"), math.floor(moveTime)) | |
print("Game loop end. Nothing more to do!") | |
def argSetup(part, **kwargs): | |
if part == "properties": propertiesSetup() | |
if part == "teams": teamsSetup(kwargs.get("config")) | |
def argStart(*args, **kwargs): | |
global teams | |
configFileName = kwargs.get("config") | |
if configFileName == None: | |
configFileName = "uhc_config.json" | |
if os.path.exists(configFileName): | |
with open(configFileName) as f: | |
mergeDict(conf, json.load(f)) | |
teamConfFileName = kwargs.get("teams") | |
if teamConfFileName == None: | |
teamConfFileName = "uhc_teams.json" | |
if os.path.exists(teamConfFileName): | |
with open(teamConfFileName) as f: | |
teams = json.load(f) | |
startTime = kwargs.get("start_time") | |
if startTime is not None: | |
conf["start_time"] = startTime | |
spawnPoint = kwargs.get("spawn_point") | |
if spawnPoint is not None: | |
x,z = spawnPoint.split(" ") | |
conf["spawn_point"]["x"] = x | |
conf["spawn_point"]["z"] = z | |
if getConf("start_time") is None: | |
print("'start_time' has to been set") | |
return | |
if os.path.exists("server.properties"): | |
with open("server.properties") as f: | |
for line in f.readlines(): | |
if line.startswith("#"): continue | |
parts = line.split("=", 1) | |
conf["prop"][parts[0]] = parts[1].rstrip() | |
gameLoop() | |
argumentTree = { | |
"name": "UHC-controller", | |
"subparsers": [ | |
{ | |
"name": "setup", | |
"cmd": argSetup, | |
"arguments": [ | |
{ | |
"name": "part", | |
"choices": ["teams", "properties"] | |
}, | |
{ | |
"name": "-config" | |
} | |
] | |
}, | |
{ | |
"name": "start", | |
"cmd": argStart, | |
"arguments": [ | |
{ | |
"name": "-start_time" | |
}, | |
{ | |
"name": "-spawn_point" | |
}, | |
{ | |
"name": "-config" | |
}, | |
{ | |
"name": "-teams" | |
} | |
] | |
} | |
] | |
} | |
parseArgTree(argumentTree) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment