SyntaxGameServer/main.py

1308 lines
55 KiB
Python

import requests
import queue
import logging
import time
import threading
import uuid
import base64
from flask import Flask, request, jsonify
import psutil
import re
import io
import random
from PIL import Image
import gzip
import os
import json
import xmltodict
from SOAPFormats import RCCSOAPMessages
from ProcessController import RccController, IsPortInUse
from ClientController import ClientController
from UDPProxy import UDPProxy
import sys
import win32gui
import win32con
try:
from config import Config
except:
if os.path.exists("C:\\Users\\Administrator\\config.py"):
sys.path.append("C:\\Users\\Administrator")
from config import Config
app = Flask(__name__)
config = Config()
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
thumbnailQueue = queue.Queue()
RCCReturnAuth = None
StopThreads = False
RunningJobs = {}
AvailableJobs = []
AvailableJobs2018 = []
AvailableJobs2020 = []
AvailableJobs2021 = []
GetNextPortMutex = threading.Lock()
GetNextRCCInstanceMutex = threading.Lock()
RCCComPort = config.RCCStartingComPort
def GetNextAvailablePort(startingPort : int, endingPort : int) -> int:
global RCCComPort
"""
Gets the next available port in the range
"""
GetNextPortMutex.acquire(timeout=20)
# Check if the com port is open
RCCComPort = random.randint(startingPort, endingPort)
if not IsPortInUse(RCCComPort):
GetNextPortMutex.release()
return RCCComPort
else:
GetNextPortMutex.release()
logging.info(f"Port {str(RCCComPort)} is not open, trying again")
return GetNextAvailablePort(startingPort, endingPort)
def RefillAvailableJobs():
global AvailableJobs
global AvailableJobs2018
global AvailableJobs2020
GetNextRCCInstanceMutex.acquire(timeout=20)
if len(AvailableJobs) > 1 and len(AvailableJobs2018) > 2:
GetNextRCCInstanceMutex.release()
return
while len(AvailableJobs) < 0:
AvailableJobs.append(RccController(config.RCCServicePath, GetNextAvailablePort(config.RCCStartingComPort, config.RCCEndingComPort), KillRCCWhenFinished=False, PlaceIdStartupBypassOverwrite = 1))
while len(AvailableJobs2018) < 0:
AvailableJobs2018.append(RccController(config.RCCService2018Path, GetNextAvailablePort(config.RCCStartingComPort, config.RCCEndingComPort), KillRCCWhenFinished=False, RCCVersion="2018", useVerbose=True))
while len(AvailableJobs2020) < 1:
AvailableJobs2020.append(RccController(config.RCCService2020Path, GetNextAvailablePort(config.RCCStartingComPort, config.RCCEndingComPort), KillRCCWhenFinished=False, RCCVersion="2020", useVerbose=True))
while len(AvailableJobs2021) < 0:
AvailableJobs2021.append(RccController(config.RCCService2021Path, GetNextAvailablePort(config.RCCStartingComPort, config.RCCEndingComPort), KillRCCWhenFinished=False, RCCVersion="2021", useVerbose=True))
GetNextRCCInstanceMutex.release()
return
def GetNextAvailableRCCInstance( version : str = "2016", startKillerWatcherThread : bool = True ) -> RccController:
global AvailableJobs
global AvailableJobs2018
"""
Gets the next available RCC instance
"""
GetNextRCCInstanceMutex.acquire(timeout=20)
if version == "2016":
if len(AvailableJobs) == 0:
GetNextRCCInstanceMutex.release()
threading.Thread(target=RefillAvailableJobs).start()
return RccController(config.RCCServicePath,GetNextAvailablePort(config.RCCStartingComPort, config.RCCEndingComPort), KillRCCWhenFinished=startKillerWatcherThread, PlaceIdStartupBypassOverwrite = 1)
AvailableInstance : RccController = AvailableJobs.pop(0)
GetNextRCCInstanceMutex.release()
elif version == "2018":
if len(AvailableJobs2018) == 0:
GetNextRCCInstanceMutex.release()
threading.Thread(target=RefillAvailableJobs).start()
return RccController(config.RCCService2018Path,GetNextAvailablePort(config.RCCStartingComPort, config.RCCEndingComPort), KillRCCWhenFinished=startKillerWatcherThread, RCCVersion="2018", useVerbose=True)
AvailableInstance : RccController = AvailableJobs2018.pop(0)
GetNextRCCInstanceMutex.release()
elif version == "2020":
if len(AvailableJobs2020) == 0:
GetNextRCCInstanceMutex.release()
threading.Thread(target=RefillAvailableJobs).start()
return RccController(config.RCCService2020Path,GetNextAvailablePort(config.RCCStartingComPort, config.RCCEndingComPort), KillRCCWhenFinished=startKillerWatcherThread, RCCVersion="2020", useVerbose=True)
AvailableInstance : RccController = AvailableJobs2020.pop(0)
GetNextRCCInstanceMutex.release()
elif version == "2021":
if len(AvailableJobs2021) == 0:
GetNextRCCInstanceMutex.release()
threading.Thread(target=RefillAvailableJobs).start()
return RccController(config.RCCService2021Path,GetNextAvailablePort(config.RCCStartingComPort, config.RCCEndingComPort), KillRCCWhenFinished=startKillerWatcherThread, RCCVersion="2021", useVerbose=True)
AvailableInstance : RccController = AvailableJobs2021.pop(0)
GetNextRCCInstanceMutex.release()
if startKillerWatcherThread:
AvailableInstance.StartKillerWatcherThread()
threading.Thread(target=RefillAvailableJobs).start()
return AvailableInstance
def thumbnailQueueWorker( workerNumber: int ):
global RunningJobs
random.seed(str(uuid.uuid4()) + str(workerNumber))
InstanceController : RccController = GetNextAvailableRCCInstance( version = "2020", startKillerWatcherThread = False)
RCCRenders : int = 0
while True:
try:
if thumbnailQueue.empty():
time.sleep(0.02)
continue
if StopThreads:
break
ThumbnailRequestInfo = thumbnailQueue.get()
logging.info(f"Processing thumbnail request: {ThumbnailRequestInfo['reqid']}, remaining queue size: {str(thumbnailQueue.qsize())}")
ThumbnailType = ThumbnailRequestInfo['type'] # 0 = PlayerThumbnail, 1 = PlayerHeadshot, 2 = Shirt or Pants, 3 = Assets ( Hats, Models etc.), 4 = Meshes
ExecuteJSON = None
Arguments = []
Expiration = 10
if ThumbnailType == 0:
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "Avatar",
"Arguments": [
f"{config.BaseURL}/v1/avatar-fetch?placeId=0&userId={ThumbnailRequestInfo['userid']}",
config.BaseURL,
"PNG",
ThumbnailRequestInfo['image_x'],
ThumbnailRequestInfo['image_y']
]
}
}
elif ThumbnailType == 1:
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "Closeup",
"Arguments": [
config.BaseURL,
f"{config.BaseURL}/v1/avatar-fetch?placeId=0&userId={ThumbnailRequestInfo['userid']}",
"PNG",
ThumbnailRequestInfo['image_x'],
ThumbnailRequestInfo['image_y'],
True,
30,
100,
0,
0
]
}
}
elif ThumbnailType == 2:
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "Avatar",
"Arguments": [
config.BaseURL,
f"{config.BaseURL}/v1/avatar-fetch/custom?assetId={ThumbnailRequestInfo['asset']}",
"PNG",
ThumbnailRequestInfo['image_x'],
ThumbnailRequestInfo['image_y'],
]
}
}
elif ThumbnailType == 3:
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "Model",
"Arguments": [
f"{config.BaseURL}/asset/?id={ThumbnailRequestInfo['asset']}",
"PNG",
ThumbnailRequestInfo['image_x'],
ThumbnailRequestInfo['image_y'],
config.BaseURL
]
}
}
elif ThumbnailType == 4:
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "Mesh",
"Arguments": [
f"{config.BaseURL}/asset/?id={ThumbnailRequestInfo['asset']}",
"PNG",
ThumbnailRequestInfo['image_x'],
ThumbnailRequestInfo['image_y'],
config.BaseURL
]
}
}
elif ThumbnailType == 5:
Expiration = 30
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "Place",
"Arguments": [
f"{config.BaseURL}/asset/?id={ThumbnailRequestInfo['asset']}",
"PNG",
ThumbnailRequestInfo['image_x'],
ThumbnailRequestInfo['image_y'],
config.BaseURL
]
}
}
elif ThumbnailType == 6:
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "Image",
"Arguments": [
ThumbnailRequestInfo['asset'],
config.BaseURL,
"PNG",
ThumbnailRequestInfo['image_x'],
ThumbnailRequestInfo['image_y']
]
}
}
elif ThumbnailType == 7:
r = requests.get(config.BaseURL + f"/Asset/?id={str(ThumbnailRequestInfo['asset'])}&access={config.AuthorizationToken}", headers={"Requester": "Server"})
if r.status_code != 200:
logging.error(f"Error downloading TShirt asset, id: {ThumbnailRequestInfo['asset']}, status code: {r.status_code}, response: {r.text}")
continue
ImageURL = re.search(r"id=(\d+)", r.text)
if not ImageURL:
ImageURL = re.search(r"rbxassetid:\/\/(\d+)", r.text)
if not ImageURL:
logging.error(f"Error finding TShirt image url, id: {ThumbnailRequestInfo['asset']}, status code: {r.status_code}, response: {r.text}")
continue
ImageId = ImageURL.group(1)
r = requests.get(f"{config.BaseURL}/asset/?id={ImageId}&access={config.AuthorizationToken}", headers={"Requester": "Server"})
if r.status_code != 200:
logging.error(f"Error downloading TShirt image, id: {ThumbnailRequestInfo['asset']}, status code: {r.status_code}, response: {r.text}")
continue
with open("./TeeShirtTemplate.png", 'rb') as bg_file:
TShirtBG = bg_file.read()
bg_image = Image.open(io.BytesIO(TShirtBG))
content_image = Image.open(io.BytesIO(r.content))
width, height = content_image.size
aspect_ratio = width / height
if width > height:
new_width = 250
new_height = int(new_width / aspect_ratio)
else:
new_height = 250
new_width = int(new_height * aspect_ratio)
content_image = content_image.resize((new_width, new_height), Image.LANCZOS)
content_image = content_image.convert("RGBA")
composite_image = Image.new('RGBA', bg_image.size)
composite_image.paste(bg_image, (0, 0))
mask = content_image.split()[3]
composite_image.paste(content_image, (85, 85), mask=mask)
composite_image_buffer = io.BytesIO()
composite_image.save(composite_image_buffer, format='PNG')
composite_image_buffer.seek(0)
# Lets just return it to ourselves
r = requests.post(
f"http://127.0.0.1:{str(config.CommPort)}/ThumbnailReturn?RCCReturnAuth={RCCReturnAuth}",
data = base64.b64encode(composite_image_buffer.getvalue()).decode('utf-8') + "|" + str(ThumbnailRequestInfo['reqid']) + "|" + str(ThumbnailRequestInfo['starttime'])
)
continue
elif ThumbnailType == 8:
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "Head",
"Arguments": [
f"{config.BaseURL}/asset/?id={ThumbnailRequestInfo['asset']}",
"PNG",
ThumbnailRequestInfo['image_x'],
ThumbnailRequestInfo['image_y'],
config.BaseURL,
1785197
]
}
}
elif ThumbnailType == 9:
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "BodyPart",
"Arguments": [
f"{config.BaseURL}/asset/?id={ThumbnailRequestInfo['asset']}",
config.BaseURL,
"PNG",
ThumbnailRequestInfo['image_x'],
ThumbnailRequestInfo['image_y'],
"http://www.roblox.com/asset/?id=1785197",
]
}
}
elif ThumbnailType == 11:
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "Hat",
"Arguments": [
f"{config.BaseURL}/asset/?id={ThumbnailRequestInfo['asset']}",
"PNG",
ThumbnailRequestInfo['image_x'],
ThumbnailRequestInfo['image_y'],
config.BaseURL
]
}
}
elif ThumbnailType == 12:
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "Gear",
"Arguments": [
f"{config.BaseURL}/asset/?id={ThumbnailRequestInfo['asset']}",
"PNG",
ThumbnailRequestInfo['image_x'],
ThumbnailRequestInfo['image_y'],
config.BaseURL
]
}
}
elif ThumbnailType == 13:
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "MeshPart",
"Arguments": [
f"{config.BaseURL}/asset/?id={ThumbnailRequestInfo['asset']}",
"PNG",
ThumbnailRequestInfo['image_x'],
ThumbnailRequestInfo['image_y'],
config.BaseURL
]
}
}
elif ThumbnailType == 14:
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "Pants",
"Arguments": [
f"{config.BaseURL}/asset/?id={ThumbnailRequestInfo['asset']}",
"PNG",
ThumbnailRequestInfo['image_x'],
ThumbnailRequestInfo['image_y'],
config.BaseURL,
1785197
]
}
}
elif ThumbnailType == 15:
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "Shirt",
"Arguments": [
f"{config.BaseURL}/asset/?id={ThumbnailRequestInfo['asset']}",
"PNG",
ThumbnailRequestInfo['image_x'],
ThumbnailRequestInfo['image_y'],
config.BaseURL,
1785197
]
}
}
elif ThumbnailType == 16:
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "Avatar_R15_Action",
"Arguments": [
config.BaseURL,
f"{config.BaseURL}/v1/avatar-fetch?placeId=0&userId={ThumbnailRequestInfo['userid']}",
"PNG",
ThumbnailRequestInfo['image_x'],
ThumbnailRequestInfo['image_y']
]
}
}
elif ThumbnailType == 17:
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "Package",
"Arguments": [
ThumbnailRequestInfo['asset'],
config.BaseURL,
"PNG",
ThumbnailRequestInfo['image_x'],
ThumbnailRequestInfo['image_y'],
"http://www.syntax.eco/asset/?id=1785197",
""
]
}
}
else:
logging.error("Invalid thumbnail type")
continue
if RCCRenders >= 5 or InstanceController.PingRCC() is False:
try:
InstanceController.KillRCC()
except:
pass
InstanceController = GetNextAvailableRCCInstance( version = "2020", startKillerWatcherThread = False)
RCCRenders = 0
ThumbnailJobId = str(uuid.uuid4())
OpenResponse : requests.Response = InstanceController.SendBatchJobRequest(JobId=ThumbnailJobId, Expiration=Expiration, Cores=1, ScriptName="Render", Arguments=Arguments, RunScript=json.dumps(ExecuteJSON), requestTimeout=Expiration+5)
RCCRenders += 1
if OpenResponse is None:
logging.error("Failed to send BatchJob request")
continue
if OpenResponse.status_code != 200:
logging.error(f"Failed to send BatchJob request, status code: {str(OpenResponse.status_code)}, response: {OpenResponse.text}")
continue
Base64Image = None
Response = xmltodict.parse(OpenResponse.text.strip())["SOAP-ENV:Envelope"]["SOAP-ENV:Body"]["ns1:BatchJobResponse"]["ns1:BatchJobResult"]
if type(Response) == list:
for ResponseItem in Response:
if ResponseItem["ns1:type"] == "LUA_TSTRING":
Base64Image = ResponseItem["ns1:value"]
else:
if Response["ns1:type"] == "LUA_TSTRING":
Base64Image = Response["ns1:value"]
if Base64Image is None:
logging.error("Failed to get image from BatchJob response")
continue
# Send it back to ourselves
uploadReq = requests.post(
f"http://127.0.0.1:{str(config.CommPort)}/ThumbnailReturn?RCCReturnAuth={RCCReturnAuth}",
data = Base64Image + "|" + str(ThumbnailRequestInfo['reqid']) + "|" + str(ThumbnailRequestInfo['starttime'])
)
#InstanceController.KillRCC()
except KeyboardInterrupt:
break
except Exception as e:
logging.error(f"Error in thumbnailQueueWorker: {e}")
time.sleep(0.02)
@app.before_request
def CheckAuthorization():
RCCReturnAuthReq = request.args.get("RCCReturnAuth")
if request.headers.get("Authorization") != Config.AuthorizationToken and RCCReturnAuthReq != RCCReturnAuth and RCCReturnAuthReq != Config.AuthorizationToken:
logging.warning(f"Unauthorized request from {request.remote_addr} to {request.path}")
return "Unauthorized", 401
@app.route("/ThumbnailReturn", methods=["POST"])
def ThumbnailReturn():
# Exepcted data:
# base64 encoded png image|reqid
try:
if request.headers.get("Content-Encoding") == "gzip":
data = gzip.decompress(request.data).decode("utf-8")
else:
data = request.data.decode("utf-8")
data = data.split("|")
reqid = data[1]
imgdata = data[0]
startime = float(data[2])
data = base64.b64decode(imgdata)
logging.info(f"Thumbnail returned for request {reqid}, took: {str(round(time.time() - startime, 2))} seconds")
req = requests.post(
config.BaseURL + "/internal/thumbnailreturn",
headers={
"Authorization": config.AuthorizationToken,
"ReturnUUID": reqid,
"Content-Type": "image/png"
},
data=data
)
return "OK", 200
except Exception as e:
logging.error(f"Error in ThumbnailReturn: {e}")
return "Error", 500
@app.route("/AssetValidation2016", methods=["POST"])
def AssetValidation2016():
"""
Expected JSON Data:
{
"assetid": 1,
}
"""
try:
InstanceController : RccController = GetNextAvailableRCCInstance()
ThumbnailJobId = str(uuid.uuid4())
with open("./Scripts/PlaceValidation.lua", "r") as f:
script = f.read()
OpenResponse : requests.Response = InstanceController.SendBatchJobRequest(
JobId=ThumbnailJobId,
Expiration=40,
Cores=1,
ScriptName="PlaceValidation",
Arguments=[
request.json['assetid'],
config.BaseURL,
config.AuthorizationToken
],
RunScript=script,
requestTimeout=45
)
if OpenResponse is None:
logging.error("Failed to send OpenJob request")
return "Error", 500
if OpenResponse.status_code != 200:
logging.error(f"Failed to send OpenJob request, status code: {str(OpenResponse.status_code)}, response: {OpenResponse.text}")
return "Error", 500
# Should only return one value which is either True as a bool or The reason as a string
PlaceValidationResponse = xmltodict.parse(OpenResponse.text.strip())["SOAP-ENV:Envelope"]["SOAP-ENV:Body"]["ns1:BatchJobResponse"]["ns1:BatchJobResult"]
if type(PlaceValidationResponse) == list:
for ResponseItem in PlaceValidationResponse:
if ResponseItem["ns1:type"] == "LUA_TBOOLEAN":
return jsonify({
"valid": ResponseItem["ns1:value"] == "true"
})
elif ResponseItem["ns1:type"] == "LUA_TSTRING":
return jsonify({
"valid": False,
"reason": ResponseItem["ns1:value"]
})
else:
if PlaceValidationResponse["ns1:type"] == "LUA_TBOOLEAN":
return jsonify({
"valid": PlaceValidationResponse["ns1:value"] == "true"
})
elif PlaceValidationResponse["ns1:type"] == "LUA_TSTRING":
return jsonify({
"valid": False,
"reason": PlaceValidationResponse["ns1:value"]
})
logging.error(f"Failed to get response from OpenJob request to RCCService, status code: {str(OpenResponse.status_code)}, response: {OpenResponse.text}")
return "Error", 500
except Exception as e:
logging.error(f"Error in AssetValidation2016: {e}")
return "Error", 500
@app.route("/AssetValidation2018", methods=["POST"])
def AssetValidation2018():
"""
Expected JSON Data:
{
"assetid": 1,
}
"""
try:
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "PlaceValidation",
"Arguments": [
f"{config.BaseURL}/asset/?id={request.json['assetid']}",
config.BaseURL
]
}
}
InstanceController : RccController = GetNextAvailableRCCInstance( version = "2018" )
ThumbnailJobId = str(uuid.uuid4())
OpenResponse : requests.Response = InstanceController.SendBatchJobRequest(JobId=ThumbnailJobId, Expiration=40, Cores=1, ScriptName="Validation", Arguments=[], RunScript=json.dumps(ExecuteJSON), requestTimeout=45)
if OpenResponse is None:
logging.error("Failed to send OpenJob request")
return "Error", 500
if OpenResponse.status_code != 200:
logging.error(f"Failed to send OpenJob request, status code: {str(OpenResponse.status_code)}, response: {OpenResponse.text}")
return "Error", 500
# Should only return one value which is either True as a bool or The reason as a string
PlaceValidationResponse = xmltodict.parse(OpenResponse.text.strip())["SOAP-ENV:Envelope"]["SOAP-ENV:Body"]["ns1:BatchJobResponse"]["ns1:BatchJobResult"]
if type(PlaceValidationResponse) == list:
for ResponseItem in PlaceValidationResponse:
if ResponseItem["ns1:type"] == "LUA_TBOOLEAN":
return jsonify({
"valid": ResponseItem["ns1:value"] == "true"
})
elif ResponseItem["ns1:type"] == "LUA_TSTRING":
return jsonify({
"valid": False,
"reason": ResponseItem["ns1:value"]
})
else:
if PlaceValidationResponse["ns1:type"] == "LUA_TBOOLEAN":
return jsonify({
"valid": PlaceValidationResponse["ns1:value"] == "true"
})
elif PlaceValidationResponse["ns1:type"] == "LUA_TSTRING":
return jsonify({
"valid": False,
"reason": PlaceValidationResponse["ns1:value"]
})
logging.error(f"Failed to get response from OpenJob request to RCCService, status code: {str(OpenResponse.status_code)}, response: {OpenResponse.text}")
return "Error", 500
except Exception as e:
logging.error(f"Error in AssetValidation2018: {e}")
return "Error", 500
@app.route("/AssetValidation2020", methods=["POST"])
def AssetValidation2020():
"""
Expected JSON Data:
{
"assetid": 1,
}
"""
try:
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "PlaceValidation",
"Arguments": [
f"{config.BaseURL}/asset/?id={request.json['assetid']}",
config.BaseURL
]
}
}
InstanceController : RccController = GetNextAvailableRCCInstance( version = "2020" )
ThumbnailJobId = str(uuid.uuid4())
OpenResponse : requests.Response = InstanceController.SendBatchJobRequest(JobId=ThumbnailJobId, Expiration=40, Cores=1, ScriptName="Validation", Arguments=[], RunScript=json.dumps(ExecuteJSON), requestTimeout=45)
if OpenResponse is None:
logging.error("Failed to send OpenJob request")
return "Error", 500
if OpenResponse.status_code != 200:
logging.error(f"Failed to send OpenJob request, status code: {str(OpenResponse.status_code)}, response: {OpenResponse.text}")
return "Error", 500
# Should only return one value which is either True as a bool or The reason as a string
PlaceValidationResponse = xmltodict.parse(OpenResponse.text.strip())["SOAP-ENV:Envelope"]["SOAP-ENV:Body"]["ns1:BatchJobResponse"]["ns1:BatchJobResult"]
if type(PlaceValidationResponse) == list:
for ResponseItem in PlaceValidationResponse:
if ResponseItem["ns1:type"] == "LUA_TBOOLEAN":
return jsonify({
"valid": ResponseItem["ns1:value"] == "true"
})
elif ResponseItem["ns1:type"] == "LUA_TSTRING":
return jsonify({
"valid": False,
"reason": ResponseItem["ns1:value"]
})
else:
if PlaceValidationResponse["ns1:type"] == "LUA_TBOOLEAN":
return jsonify({
"valid": PlaceValidationResponse["ns1:value"] == "true"
})
elif PlaceValidationResponse["ns1:type"] == "LUA_TSTRING":
return jsonify({
"valid": False,
"reason": PlaceValidationResponse["ns1:value"]
})
logging.error(f"Failed to get response from OpenJob request to RCCService, status code: {str(OpenResponse.status_code)}, response: {OpenResponse.text}")
return "Error", 500
except Exception as e:
logging.error(f"Error in AssetValidation2020: {e}")
return "Error", 500
@app.route("/AssetValidation2021", methods=["POST"])
def AssetValidation2021():
"""
Expected JSON Data:
{
"assetid": 1,
}
"""
try:
ExecuteJSON = {
"Mode": "Thumbnail",
"Settings": {
"Type": "PlaceValidation",
"Arguments": [
f"{config.BaseURL}/asset/?id={request.json['assetid']}",
config.BaseURL
]
}
}
InstanceController : RccController = GetNextAvailableRCCInstance( version = "2021" )
ThumbnailJobId = str(uuid.uuid4())
OpenResponse : requests.Response = InstanceController.SendBatchJobRequest(JobId=ThumbnailJobId, Expiration=40, Cores=1, ScriptName="Validation", Arguments=[], RunScript=json.dumps(ExecuteJSON), requestTimeout=45)
if OpenResponse is None:
logging.error("Failed to send OpenJob request")
return "Error", 500
if OpenResponse.status_code != 200:
logging.error(f"Failed to send OpenJob request, status code: {str(OpenResponse.status_code)}, response: {OpenResponse.text}")
return "Error", 500
# Should only return one value which is either True as a bool or The reason as a string
PlaceValidationResponse = xmltodict.parse(OpenResponse.text.strip())["SOAP-ENV:Envelope"]["SOAP-ENV:Body"]["ns1:BatchJobResponse"]["ns1:BatchJobResult"]
if type(PlaceValidationResponse) == list:
for ResponseItem in PlaceValidationResponse:
if ResponseItem["ns1:type"] == "LUA_TBOOLEAN":
return jsonify({
"valid": ResponseItem["ns1:value"] == "true"
})
elif ResponseItem["ns1:type"] == "LUA_TSTRING":
return jsonify({
"valid": False,
"reason": ResponseItem["ns1:value"]
})
else:
if PlaceValidationResponse["ns1:type"] == "LUA_TBOOLEAN":
return jsonify({
"valid": PlaceValidationResponse["ns1:value"] == "true"
})
elif PlaceValidationResponse["ns1:type"] == "LUA_TSTRING":
return jsonify({
"valid": False,
"reason": PlaceValidationResponse["ns1:value"]
})
logging.error(f"Failed to get response from OpenJob request to RCCService, status code: {str(OpenResponse.status_code)}, response: {OpenResponse.text}")
return "Error", 500
except Exception as e:
logging.error(f"Error in AssetValidation2020: {e}")
return "Error", 500
@app.route("/Thumbnail", methods=["POST"])
def Thumbnail():
# Expected json data:
# {
# "userid": 1, - needed for type 0 and 1
# "type": 1,
# "image_x": 512,
# "image_y": 512,
# "reqid": "uuid4"
# }
try:
data = request.json
data['starttime'] = time.time()
if data['type'] in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17]:
thumbnailQueue.put(data)
logging.info(f"Thumbnail request queued, id: {data['reqid']}, type: {data['type']}")
return "OK", 200
else:
logging.error(f"Invalid thumbnail type: {data['type']}")
return "Invalid thumbnail type", 400
except Exception as e:
logging.error(f"Error in Thumbnail: {e}")
return "Error", 500
NextAvilablePort = config.RCCStartingPort
@app.route("/Game2014", methods=["POST"])
def Game2014():
global NextAvilablePort
"""
Expected JSON Data:
{
"placeid": 1,
"creatorId": 1,
"creatorType": 1
}
"""
try:
GameOpenData = request.json
ServerJobId = str(uuid.uuid4())
ServerPort = NextAvilablePort
NextAvilablePort += 1
if NextAvilablePort > config.RCCEndingPort:
NextAvilablePort = config.RCCStartingPort
RCC_UDP_Proxy = None
try:
RCC_UDP_Proxy = UDPProxy(
UDPProxyPort=ServerPort,
UDPProxyTargetHost="127.0.0.1",
UDPProxyTargetPort=ServerPort + config.PortOffset
)
RCC_UDP_Proxy.StartUDPProxy()
except Exception as e:
logging.error(f"Error creating UDPProxy: {e}")
return "Error", 500
try:
InstanceController : ClientController = ClientController(
ExecutablePath = config.Client2014Path,
JoinscriptUrl = f"{config.BaseURL}/game/gameserver2014.lua?placeId={GameOpenData['placeid']}&networkPort={ServerPort + config.PortOffset}&creatorId={GameOpenData['creatorId']}&creatorType={GameOpenData['creatorType']}&jobId={ServerJobId}",
ExpectedPort = ServerPort + config.PortOffset,
StartTimeout = 40
)
except Exception as e:
logging.error("Failed to start 2014Client Controller in Game2014, error: " + str(e))
RCC_UDP_Proxy.StopUDPProxy()
return "Error", 500
RunningJobs[ServerJobId] = InstanceController
if RCC_UDP_Proxy is not None:
InstanceController.BindUDPProxy(RCC_UDP_Proxy)
logging.info(f"Game server opened, jobid: {ServerJobId}, Port Forwarded: {ServerPort}, Actual Port: {ServerPort + config.PortOffset}")
return jsonify({
"jobid": ServerJobId,
"port": ServerPort
})
except Exception as e:
logging.error(f"Error in Game2014: {e}")
return "Error", 500
@app.route("/Game", methods=["POST"])
def Game():
global RunningJobs
global NextAvilablePort
# Expected json data:
# {
# "placeid": 1,
# "creatorId": 1,
# "creatorType": 1,
# "useNewLoadFile": true
# "loadfile_location": "https://www.syntax.eco/game/gameserver2016.lua"
# }
try:
GameOpenData = request.json
if not GameOpenData['useNewLoadFile']:
with open("./Scripts/Gameserver.lua", "r") as f:
script = f.read()
else:
script = f"loadfile(\"{GameOpenData['loadfile_location']}\")(...)"
ServerJobId = str(uuid.uuid4())
ServerPort = NextAvilablePort
NextAvilablePort += 1
if NextAvilablePort > config.RCCEndingPort:
NextAvilablePort = config.RCCStartingPort
RCC_UDP_Proxy = None
try:
RCC_UDP_Proxy = UDPProxy(
UDPProxyPort=ServerPort,
UDPProxyTargetHost="127.0.0.1",
UDPProxyTargetPort=ServerPort + config.PortOffset
)
RCC_UDP_Proxy.StartUDPProxy()
except Exception as e:
logging.error(f"Error creating UDPProxy: {e}")
return "Error", 500
InstanceController : RccController = GetNextAvailableRCCInstance()
OpenJobResponse : requests.Response = InstanceController.SendOpenJobRequest(
JobId=ServerJobId,
Expiration=60*60*24, # 24 hours
Cores=2,
ScriptName="GameServer",
RunScript=script,
Arguments=[
GameOpenData['placeid'],
(ServerPort + config.PortOffset) if RCC_UDP_Proxy is not None else ServerPort,
config.BaseURL,
config.AuthorizationToken,
GameOpenData['creatorId'],
GameOpenData['creatorType'],
GameOpenData['SpecialAccessToken'],
GameOpenData['universeid'] if 'universeid' in GameOpenData else GameOpenData['placeid']
]
)
if OpenJobResponse.status_code != 200:
logging.error(f"Error sending OpenJob request to RCCService, status code: {OpenJobResponse.status_code}, response: {OpenJobResponse.text}")
InstanceController.KillRCC()
if RCC_UDP_Proxy is not None:
RCC_UDP_Proxy.StopUDPProxy()
return "Error", 500
RunningJobs[ServerJobId] = InstanceController
if RCC_UDP_Proxy is not None:
InstanceController.BindUDPProxy(RCC_UDP_Proxy)
logging.info(f"Game server opened, jobid: {ServerJobId}, Port Forwarded: {ServerPort}, Actual Port: {ServerPort + config.PortOffset}")
return jsonify({
"jobid": ServerJobId,
"port": ServerPort
})
except Exception as e:
logging.error(f"Error in Game: {e}")
return "Error", 500
@app.route("/Game2018", methods=["POST"])
def Game2018():
global RunningJobs
global NextAvilablePort
"""
Expected JSON Data:
{
"placeid": 1,
"creatorId": 1,
"creatorType": "User",
"jobid": "uuid4",
"apikey": "apikey",
"maxplayers": 10,
"address": "127.0.0.1"
}
"""
try:
GameOpenData = request.json # We trust the game server to send us the correct data :)
ServerPort = NextAvilablePort
NextAvilablePort += 1
if NextAvilablePort > config.RCCEndingPort:
NextAvilablePort = config.RCCStartingPort
RCC_UDP_Proxy = None
try:
RCC_UDP_Proxy = UDPProxy(
UDPProxyPort=ServerPort,
UDPProxyTargetHost="127.0.0.1",
UDPProxyTargetPort=ServerPort + config.PortOffset
)
RCC_UDP_Proxy.StartUDPProxy()
except Exception as e:
logging.error(f"Error creating UDPProxy: {e}")
return "Error", 500
InstanceController : RccController = GetNextAvailableRCCInstance( version = "2018" )
RCCFormatter = RCCSOAPMessages()
GameOpenJSON = RCCFormatter.FormatGameOpenJSON(
PlaceId = GameOpenData['placeid'],
CreatorId = GameOpenData['creatorId'],
CreatorType = GameOpenData['creatorType'],
JobId = GameOpenData['jobid'],
ApiKey = GameOpenData['apikey'],
MaxPlayers = GameOpenData['maxplayers'],
PortNumber = (ServerPort + config.PortOffset) if RCC_UDP_Proxy is not None else ServerPort,
MachineAddress = "127.0.0.1" if RCC_UDP_Proxy is not None else GameOpenData['address'],
UniverseId = GameOpenData['universeid'] if 'universeid' in GameOpenData else GameOpenData['placeid']
)
OpenJobResponse : requests.Response = InstanceController.SendOpenJobRequest(
JobId=GameOpenData['jobid'],
Expiration=60*60*24, # 24 hours
Cores=1,
ScriptName="GameServer",
RunScript=GameOpenJSON,
Arguments=[]
)
if OpenJobResponse.status_code != 200:
logging.error(f"Error sending OpenJob request to RCCService, status code: {OpenJobResponse.status_code}, response: {OpenJobResponse.text}")
InstanceController.KillRCC()
if RCC_UDP_Proxy is not None:
RCC_UDP_Proxy.StopUDPProxy()
return "Error", 500
RunningJobs[GameOpenData['jobid']] = InstanceController
if RCC_UDP_Proxy is not None:
InstanceController.BindUDPProxy(RCC_UDP_Proxy)
logging.info(f"2018 Game server opened, jobid: {GameOpenData['jobid']}, Port Forwarded: {ServerPort}, Actual Port: {ServerPort + config.PortOffset}")
return jsonify({
"jobid": GameOpenData['jobid'],
"port": ServerPort
})
except Exception as e:
logging.error(f"Error in Game2018: {e}")
return "Error", 500
@app.route("/Game2020", methods=["POST"])
def Game2020():
global RunningJobs
global NextAvilablePort
"""
Expected JSON Data:
{
"placeid": 1,
"creatorId": 1,
"creatorType": "User",
"jobid": "uuid4",
"apikey": "apikey",
"maxplayers": 10,
"address": "127.0.0.1"
}
"""
try:
GameOpenData = request.json # We trust the game server to send us the correct data :)
ServerPort = NextAvilablePort
NextAvilablePort += 1
if NextAvilablePort > config.RCCEndingPort:
NextAvilablePort = config.RCCStartingPort
RCC_UDP_Proxy = None
try:
RCC_UDP_Proxy = UDPProxy(
UDPProxyPort=ServerPort,
UDPProxyTargetHost="127.0.0.1",
UDPProxyTargetPort=ServerPort + config.PortOffset
)
RCC_UDP_Proxy.StartUDPProxy()
except Exception as e:
logging.error(f"Error creating UDPProxy: {e}")
return "Error", 500
InstanceController : RccController = GetNextAvailableRCCInstance( version = "2020" )
RCCFormatter = RCCSOAPMessages()
GameOpenJSON = RCCFormatter.FormatGameOpenJSON(
PlaceId = GameOpenData['placeid'],
CreatorId = GameOpenData['creatorId'],
CreatorType = GameOpenData['creatorType'],
JobId = GameOpenData['jobid'],
ApiKey = GameOpenData['apikey'],
MaxPlayers = GameOpenData['maxplayers'],
PortNumber = (ServerPort + config.PortOffset) if RCC_UDP_Proxy is not None else ServerPort,
MachineAddress = "127.0.0.1" if RCC_UDP_Proxy is not None else GameOpenData['address'],
UniverseId = GameOpenData['universeid'] if 'universeid' in GameOpenData else GameOpenData['placeid']
)
OpenJobResponse : requests.Response = InstanceController.SendOpenJobRequest(
JobId=GameOpenData['jobid'],
Expiration=60*60*24, # 24 hours
Cores=1,
ScriptName="GameServer",
RunScript=GameOpenJSON,
Arguments=[]
)
if OpenJobResponse.status_code != 200:
logging.error(f"Error sending OpenJob request to RCCService, status code: {OpenJobResponse.status_code}, response: {OpenJobResponse.text}")
InstanceController.KillRCC()
if RCC_UDP_Proxy is not None:
RCC_UDP_Proxy.StopUDPProxy()
return "Error", 500
RunningJobs[GameOpenData['jobid']] = InstanceController
if RCC_UDP_Proxy is not None:
InstanceController.BindUDPProxy(RCC_UDP_Proxy)
logging.info(f"2020 Game server opened, jobid: {GameOpenData['jobid']}, Port Forwarded: {ServerPort}, Actual Port: {ServerPort + config.PortOffset}")
return jsonify({
"jobid": GameOpenData['jobid'],
"port": ServerPort
})
except Exception as e:
logging.error(f"Error in Game2020: {e}")
return "Error", 500
@app.route("/Game2021", methods=["POST"])
def Game2021():
global RunningJobs
global NextAvilablePort
"""
Expected JSON Data:
{
"placeid": 1,
"creatorId": 1,
"creatorType": "User",
"jobid": "uuid4",
"apikey": "apikey",
"maxplayers": 10,
"address": "127.0.0.1"
}
"""
try:
GameOpenData = request.json # We trust the game server to send us the correct data :)
ServerPort = NextAvilablePort
NextAvilablePort += 1
if NextAvilablePort > config.RCCEndingPort:
NextAvilablePort = config.RCCStartingPort
RCC_UDP_Proxy = None
try:
RCC_UDP_Proxy = UDPProxy(
UDPProxyPort=ServerPort,
UDPProxyTargetHost="127.0.0.1",
UDPProxyTargetPort=ServerPort + config.PortOffset
)
RCC_UDP_Proxy.StartUDPProxy()
except Exception as e:
logging.error(f"Error creating UDPProxy: {e}")
return "Error", 500
InstanceController : RccController = GetNextAvailableRCCInstance( version = "2021" )
RCCFormatter = RCCSOAPMessages()
GameOpenJSON = RCCFormatter.FormatGameOpenJSON(
PlaceId = GameOpenData['placeid'],
CreatorId = GameOpenData['creatorId'],
CreatorType = GameOpenData['creatorType'],
JobId = GameOpenData['jobid'],
ApiKey = GameOpenData['apikey'],
MaxPlayers = GameOpenData['maxplayers'],
PortNumber = (ServerPort + config.PortOffset) if RCC_UDP_Proxy is not None else ServerPort,
MachineAddress = "127.0.0.1" if RCC_UDP_Proxy is not None else GameOpenData['address'],
UniverseId = GameOpenData['universeid'] if 'universeid' in GameOpenData else GameOpenData['placeid']
)
OpenJobResponse : requests.Response = InstanceController.SendOpenJobRequest(
JobId=GameOpenData['jobid'],
Expiration=60*60*24, # 24 hours
Cores=1,
ScriptName="GameServer",
RunScript=GameOpenJSON,
Arguments=[]
)
if OpenJobResponse.status_code != 200:
logging.error(f"Error sending OpenJob request to RCCService, status code: {OpenJobResponse.status_code}, response: {OpenJobResponse.text}")
InstanceController.KillRCC()
if RCC_UDP_Proxy is not None:
RCC_UDP_Proxy.StopUDPProxy()
return "Error", 500
RunningJobs[GameOpenData['jobid']] = InstanceController
if RCC_UDP_Proxy is not None:
InstanceController.BindUDPProxy(RCC_UDP_Proxy)
logging.info(f"2021 Game server opened, jobid: {GameOpenData['jobid']}, Port Forwarded: {ServerPort}, Actual Port: {ServerPort + config.PortOffset}")
return jsonify({
"jobid": GameOpenData['jobid'],
"port": ServerPort
})
except Exception as e:
logging.error(f"Error in Game2021: {e}")
return "Error", 500
@app.route("/Execute", methods=["POST"])
def ExecuteScript():
global RunningJobs
try:
data = request.json
script = data['script']
arguments = data['arguments'] if 'arguments' in data else []
scriptname = data['scriptname'] if 'scriptname' in data else "RunScript"
jobid = data['jobid'] if 'jobid' in data else None
if jobid is None:
return "Error", 400
if jobid not in RunningJobs:
return "Error", 400
InstanceController : RccController = RunningJobs[jobid]
if InstanceController.PingRCC() == False:
del RunningJobs[jobid]
return "Error", 400
ExecuteScriptResponse : requests.Response = InstanceController.SendExecuteScriptRequest(jobid, scriptname, script, arguments)
if ExecuteScriptResponse.status_code != 200:
logging.error(f"Error sending ExecuteScript request to RCCService, status code: {ExecuteScriptResponse.status_code}, response: {ExecuteScriptResponse.text}")
return "Error", 500
return "OK", 200
except Exception as e:
logging.error(f"Error in Execute: {e}")
return "Error", 500
@app.route("/CloseJob", methods=["POST"])
def CloseJob():
global RunningJobs
try:
data = request.json
jobid = data['jobid']
if jobid not in RunningJobs:
try:
del RunningJobs[jobid]
except:
pass
return "OK", 200
InstanceController : RccController | ClientController = RunningJobs[jobid]
if type(InstanceController) == ClientController:
if InstanceController.Process.poll() is None:
InstanceController.KillRCC()
del RunningJobs[jobid]
return "OK", 200
if InstanceController.PingRCC() == False:
del RunningJobs[jobid]
return "OK", 200
CloseJobResposne : requests.Response = InstanceController.SendCloseJobRequest(jobid)
if CloseJobResposne is None:
# this means that the RCC is already dead
del RunningJobs[jobid]
return "OK", 200
if CloseJobResposne.status_code != 200:
logging.error(f"Error sending CloseJob request to RCCService, status code: {CloseJobResposne.status_code}, response: {CloseJobResposne.text}")
return "Error", 500
del RunningJobs[jobid]
InstanceController.KillRCC()
logging.info(f"Game server closed, jobid: {jobid}")
return "OK", 200
except Exception as e:
logging.error(f"Error in CloseJob: {e}")
return "Error", 500
def CollectDeadProcess():
try:
running_servers : list[psutil.Process] = []
processes = psutil.process_iter()
for process in processes:
if "RCCService" in process.name() or "SyntaxPlayerBeta" in process.name():
running_servers.append(process)
for active_server in running_servers:
if active_server.memory_info().rss / 1024 ** 2 > 3000:
active_server.kill()
logging.info(f"Killed process {active_server.pid} because it was using too much memory")
continue
if active_server.status() in [psutil.STATUS_DEAD, psutil.STATUS_STOPPED, psutil.STATUS_ZOMBIE, psutil.STATUS_IDLE]:
active_server.kill()
logging.info(f"Killed process {active_server.pid} because it was in {str(active_server.status())} state")
continue
if "SyntaxPlayerBeta" in active_server.name():
if active_server.cpu_percent() > 90:
active_server.kill()
logging.info(f"Killed process {active_server.pid} because it was using too much cpu")
continue
if active_server.create_time() < time.time() - 60 * 60 * 24:
active_server.kill()
logging.info(f"Killed process {active_server.pid} because it was running for too long")
continue
def EnumWindowsCallback( hwnd, lParam ):
if win32gui.GetWindowText(hwnd) == "ROBLOX Crash":
win32gui.PostMessage(hwnd, win32con.WM_CLOSE, 0, 0)
return True
win32gui.EnumWindows(EnumWindowsCallback, 0)
except Exception as e:
logging.error(f"Error in CollectDeadProcess: {e}")
def RunCollectDeadProcessWorker():
while True:
try:
CollectDeadProcess()
except Exception as e:
logging.error(f"Error in CollectDeadProcessWorker: {e}")
time.sleep(25)
@app.route("/stats", methods=["GET"])
def Stats():
global RunningJobs
RCCMemoryUsage = 0
while True:
try:
for jobid in RunningJobs:
InstanceController : RccController | ClientController = RunningJobs[jobid]
if type(InstanceController) == ClientController:
if InstanceController.Process.poll() is not None:
del RunningJobs[jobid]
continue
RCCMemoryUsage += psutil.Process(InstanceController.Process.pid).memory_info().rss / 1024 ** 2
def EnumWindowsCallback( hwnd, lParam ):
if win32gui.GetWindowText(hwnd) == "ROBLOX":
win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE)
return True
win32gui.EnumWindows(EnumWindowsCallback, 0)
else:
if InstanceController.PingRCC() == False:
del RunningJobs[jobid]
continue
RCCMemoryUsage += psutil.Process(InstanceController.RCCProcess.pid).memory_info().rss / 1024 ** 2
break
except Exception as e:
pass
ListOfRunningJobs = []
for jobid in RunningJobs:
ListOfRunningJobs.append(jobid)
return jsonify({
"RCCOnline": True, #isRCCOnline(), This was before we created a new rcc instance for each job but we leave it here for now
"RCCMemoryUsage": RCCMemoryUsage,
"ThumbnailQueueSize": thumbnailQueue.qsize(),
"RunningJobs": ListOfRunningJobs
})
if __name__ == "__main__":
try:
RCCReturnAuth = str(uuid.uuid4())
logging.info("RCCReturnAuth: " + RCCReturnAuth)
for i in range(config.ThumbnailWorkerCount):
logging.info(f"Starting thumbnailQueueWorker {str(i)}")
thumbnailQueueWorkerThread = threading.Thread(target=thumbnailQueueWorker, args=(i, ))
time.sleep(0.1)
thumbnailQueueWorkerThread.start()
threading.Thread(target=RefillAvailableJobs).start()
threading.Thread(target=RunCollectDeadProcessWorker).start()
app.run(
host="0.0.0.0",
port=Config.CommPort,
debug=False
)
StopThreads = True
except KeyboardInterrupt:
StopThreads = True