######################################################################################## # This file is part of Spheres. # Copyright (C) 2019 Jesusalva # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA ######################################################################################## # Definitions init -3 python: renpy.add_python_directory("python-extra") import requests, zlib, base64, sys, copy import websock as wsock # set PYTHON_VERSION variable (should be 2713, 3605 could fail) PYTHON_VERSION="%d%d%02d" % (sys.version_info.major, sys.version_info.minor, sys.version_info.micro) PYTHON_VERSION=int(PYTHON_VERSION) # Ren'Py should come with Python 2.7.10 (2710), but just in case # After all, I only tested with 2.7.10, 2.7.13 and 2.7.15 if (PYTHON_VERSION < 2700 or PYTHON_VERSION > 3000): raise Exception("WARNING: Python version is not 2.7\nStrange bugs may happen on your client.\n\nClick on \"Ignore\" to continue.\nClick on \"Quit\" to exit.") # Why setting this...? ApTimer="" # Configuration config.autoreload = False config.save_on_mobile_background = False persistent.release_name = "Renewal" if persistent.host is None: persistent.host="spheres.tmw2.org" # FIXME: Set good defaults (=bad) for Android if renpy.android: persistent.nothreading=True persistent.fatality=True if (persistent.allfiles is None): persistent.allfiles=[] allfiles=[] HOST=str(persistent.host) PORT=10301 UPDP=10302 FAILUREMSG="Request failed, Please try again" OFFLINEMSG="401 Unauthorized" OKMSG="200 OK" TIMEOUT_INTERVAL=3.0 MAX_IRC_BUFFER=50 SSL_IS_BROKEN=False debug=copy.copy(config.developer) # Error Codes ERR_JSONDECODER=101 ERR_LOGIN_DEFAULT=102 ERR_INVALID=103 ERR_TIMEOUT=104 ERR_NOGEMS=105 ERR_INVFULL=106 ERR_OK=200 # All error code library ERRNO=[FAILUREMSG, OFFLINEMSG, OKMSG, ERR_LOGIN_DEFAULT, ERR_INVALID, ERR_TIMEOUT, ERR_NOGEMS, ERR_INVFULL, ERR_OK] # Core musics/sfx MUSIC_OPENING="sfx/bgm01.mp3" MUSIC_TOWN="sfx/bgm02.mp3" # Spheres actions AP_NONE =False AP_SPHERE =1 AP_SKILL =2 # Status ST_TOWN =0 ST_QUEST =1 # Unit flags UF_NOLVL =1 UF_NOPART =2 UF_EXPUP =4 UF_EVOMAT =8 UF_SUPEREVO =64 # Jobs Job_Swordsman =1 Job_Assassin =2 Job_Mage =3 Job_Archer =4 Job_Gunner =5 # IRC flags IRC_AUTH_NONE =0 IRC_AUTH_USER =1 IRC_AUTH_NICK =2 IRC_AUTH_CHAN =3 # Smart Print command def stdout(message): if debug: if renpy.android: if not renpy.is_init_phase(): renpy.notify(message) else: print(message) renpy.write_log("[DEBUG] %s" % message) else: renpy.write_log("[GAME] %s" % message) return # Global classes # We need to override standard list method. Original by Triptych (stackoverflow) class dlist(list): def __setitem__(self, index, value): size = len(self) if index >= size: self.extend(None for _ in range(size, index + 1)) list.__setitem__(self, index, value) class ExecuteOnCall(): def __init__(self, callable, *args, **kwargs): self.callable = callable self.args = args self.kwargs = kwargs def __call__(self): rv = self.callable(*self.args, **self.kwargs) return rv def id(self): return self.__call__() class RetString(): def __init__(self, string): self.string = string def __call__(self): return self.string def id(self): return self.__call__() # Screen Functions/class # Override class SpheresMainMenu(MainMenu): def __call__(self): if not self.get_sensitive(): return if self.confirm: if config.autosave_on_quit: renpy.force_autosave() layout.yesno_screen(layout.MAIN_MENU, SpheresMainMenu(False)) else: # Flush labels/sockets/timers as needed renpy.call_in_new_context("quit") # Restart renpy.full_restart(config.game_main_transition) class ExtraImage(renpy.display.im.Image): """ Custom image manipulator, based on bink's code, topic 11732 """ def __init__(self, loc, **properties): """ @param loc: Where the image really is (get_path already applied) """ super(ExtraImage, self).__init__(loc, **properties) self.loc = loc def load(self, unscaled=False): # W0221 try: #page = open(self.loc, "rb") #pd = page.read() #picdata = os.tmpfile() #picdata.write(pd) #picdata.seek(0) # reset seek position stdout("Requested to open: %s" % repr(self.loc)) picdata = open(self.loc, "rb") stdout("Picdata is open (%s)" % self.loc) if unscaled: surf = renpy.display.pgrender.load_image_unscaled(picdata, self.loc) else: surf = renpy.display.pgrender.load_image(picdata, self.loc) stdout("Picdata was closed") picdata.close() #page.close() return surf except Exception, e: if renpy.config.missing_image_callback: im = renpy.config.missing_image_callback(self.loc) if im is None: raise e return im.load() raise def virtpos(posix): if isinstance(posix, float): return posix*1024 else: return posix/1024.0 # File Managment Functions def get_path(path): if renpy.android: #print "Android detected" path=path.replace("/", "_") #return renpy.loader.get_path(path) return renpy.config.savedir + "/" + path else: return renpy.loader.get_path(path) def get_path_if_exists(path): if renpy.android: print "Android detected, not checking" path=path.replace("/", "_") #return renpy.loader.get_path(path) return renpy.config.savedir + "/" + path else: return renpy.loader.transfn(path) # URL3 Function def GAME_UPDATER(): global tr_load tr_load=False # If no version is provided, we are using default files # Default files version is "1" (Should never happen) if (persistent.version is None): persistent.version=1 # Download upstream version x=requests.get("http://"+HOST+'/version.txt') try: ver=int(x.text) except: stdout("IMPOSSIBLE TO DETERMINE VERSION") raise Exception("Could not estabilish a connection to update server:\n%s is not valid." % x.text) # TODO: Show this beautifully? # TODO: Should we set a "ver"? if (int(persistent.version) < ver): # Check if the server have SSL support try: stdout("Downloading certificate from server") x=requests.get("http://"+HOST+'/certificate.pem') #print len(x.text) if "BEGIN CERTIFICATE" in x.text: persistent.ssl_enabled=True stdout("Updating server certificate") f=open(get_path("cert/certificate.pem"), "w") f.write(x.text) f.close() stdout("SSL: ENABLED") else: raise Exception("Not a certificate") except: stdout("SSL: DISABLED") persistent.ssl_enabled=False # Download quests.json f=open(get_path("quests.json"), "w") stdout("Downloading quests.json") x=requests.get("http://"+HOST+'/quests.json') f.write(x.text) f.close() # Download units.json f=open(get_path("units.json"), "w") stdout("Downloading units.json") x=requests.get("http://"+HOST+'/units.json') f.write(x.text) f.close() # Download story.json f=open(get_path("story.json"), "w") stdout("Downloading story.json") x=requests.get("http://"+HOST+'/story.json') f.write(x.text) f.close() # Download world.json f=open(get_path("world.json"), "w") stdout("Downloading world.json") x=requests.get("http://"+HOST+'/world.json') f.write(x.text) f.close() # Download bar.json f=open(get_path("bar.json"), "w") stdout("Downloading bar.json") x=requests.get("http://"+HOST+'/bar.json') f.write(x.text) f.close() # Download summons.json f=open(get_path("summons.json"), "w") stdout("Downloading summons.json") x=requests.get("http://"+HOST+'/summons.json') f.write(x.text) f.close() persistent.version=ver stdout("Update complete") # Download server news # Handled by GAME_LOADER tr_load=True return tr_load ############################################################################ init -1 python: import socket, sys, time, json if renpy.android: try: import androidssl as ssl except: SSL_IS_BROKEN=True import ssl stdout("Broken Android SSL detected, FALLING BACK") else: import ssl print("Using system-wide SSL implementation...") print("======================= %s %s %s" % (config.name, config.version, persistent.release_name)) print "[STDBY] Loading Basic functions......." # Search for array[?][key]==search in an array of dicts # Returns the dictionary, or returns ERR_INVALID def dl_search(array, key, search): try: r=(item for item in array if item[key] == search).next() except: r=ERR_INVALID if r is None: r=ERR_INVALID stdout("dlsearch: r is None") return r def check_fail(raw): global debug, FAILUREMSG if (debug): stdout(str(raw)) if (raw == FAILUREMSG): return True return False def json_decode(raw): global ERRNO if (check_fail(raw)): return ERR_LOGIN_DEFAULT # TODO Move this to check_fail and rewrite check_fail # ERR_OFF should be handled top-level no? With full_restart() if raw in ERRNO: return raw try: return int(raw) except: pass # Maybe base 64 try: rw=base64.b64decode(raw) raw=rw if (debug): print "base64 decoded" except: pass # Maybe zlib compressed try: rw=zlib.decompress(raw) raw=rw if (debug): print str(raw) except: pass # Decode JSON try: return json.loads(raw) except: return ERR_JSONDECODER def get_token(): try: t=Player['token'] except: t="0" #"f528764d624db129b32c21fbca0cb8d6" return t def login(): global Player stdout("Login requested for %s" % logindata()) raw=send_packet("login", logindata()) Player=json_decode(raw) if (Player == ERR_JSONDECODER): return ERR_JSONDECODER if (Player == ERR_LOGIN_DEFAULT): return ERR_LOGIN_DEFAULT try: Player["inv"]=dlist() except: pass return Player["code"] def send_packet_now(packet, args="", legacy=False): global tr_load, tr_val try: sock80 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if not renpy.android: sock80.settimeout(10) else: stdout("RAW Socket Ready") # TODO: Request server if it is running with SSL or not # If SSL is enabled (server configuration) if persistent.ssl_enabled: if not SSL_IS_BROKEN: # Create the contest context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) # Protect the most possible, if appliable. context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 # FIXME We should check it if persistent.ssl_enabled != "IGNORE": context.check_hostname = True context.verify_mode = ssl.CERT_OPTIONAL else: stdout("[SECURITY] SSL VERIFICATION DISABLED.") context.check_hostname = False context.verify_mode = ssl.CERT_NONE #context.load_default_certs() # <- A .crt file context.load_verify_locations(get_path("cert/certificate.pem")) stdout("SSL Context Ready") sock = context.wrap_socket(sock80, server_hostname=HOST) else: CIPHERS_OVERRIDE = ":".join( [ "ECDHE+AESGCM", #"ECDHE+CHACHA20", #"DHE+AESGCM", #"DHE+CHACHA20", "ECDH+AESGCM", #"DH+AESGCM", "ECDH+AES", #"DH+AES", "RSA+AESGCM", "RSA+AES", #"!aNULL", #"!eNULL", #"!MD5", #"!DSS", ] ) if persistent.ssl_enabled != "IGNORE": sock = ssl.wrap_socket(sock80, ca_certs=get_path("cert/certificate.pem"), ssl_version=2, cert_reqs=ssl.CERT_OPTIONAL, ciphers=CIPHERS_OVERRIDE) else: stdout("[SECURITY] SSL VERIFICATION DISABLED.") sock = ssl.wrap_socket(sock80, ca_certs=get_path("cert/certificate.pem"), ssl_version=2, cert_reqs=ssl.CERT_NONE, ciphers=CIPHERS_OVERRIDE) stdout("Android SSL Wrapper Ready") else: sock=sock80 if not renpy.android: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) else: stdout("Socket Ready") sock.connect((HOST, PORT)) if renpy.android: stdout("Socket Ready") stdout("Connected: (%s, %d), Socket %s" % (HOST, PORT, sock.getsockname())) renpy.pause(1.0) #m=get_token() + ";" + packet m=get_token() + ";" + packet + ";" + args stdout("Sending: %s" % m) sock.sendall(m) # Receive data from the server received = sock.recv(2048) stdout("received: %s" % received) # Close the socket sock.shutdown(socket.SHUT_RDWR) sock.close() except: received=FAILUREMSG if persistent.fatality is not None: raise # Legacy support (for ping) if legacy: return received tr_load=True tr_val=str(received) return def send_packet(packet, args=""): global tr_load, tr_val # This is a secret variable which disables threading # This may cause hangs and other issues. if persistent.nothreading: send_packet_now(packet, args) val=tr_val return val timeout=0.0 tr_load=False tr_val=None renpy.show("spinner", at_list=[truecenter]) renpy.invoke_in_thread(send_packet_now, packet, args) while not tr_load: renpy.pause(0.1) timeout+=0.1 if timeout >= TIMEOUT_INTERVAL: # FIXME: What if a screen is already being displayed? BUG try: renpy.call_screen("msgbox", "Error Code: %d\n\nApplication timeout, click to try again" % (ERR_TIMEOUT)) timeout=0.0 except: if not "ping" in packet.lower(): stdout("WARNING, ILLEGAL PACKET ON SCREEN TIME: %s" % packet) pass renpy.hide("spinner") val=tr_val del tr_val print "value obtained" if (val is None): return ERR_INVALID return val def GAME_LOADER(): global allunitsbase, allunits, allquests, allstory, allworld, alltaverns global allnews, tr_load tr_load=False # Load unit data if renpy.android: allunitsbase=json.loads(requests.get("http://"+HOST+'/units.json').text) else: f=open(get_path_if_exists("units.json"), "r") allunitsbase=json.load(f) f.close() # Reorder unit data allunits={} for j in allunitsbase: allunits[j["unit_id"]]=j # Load summons data f=open(get_path_if_exists("summons.json"), "r") allworld=json.load(f) f.close() # Load quest data f=open(get_path_if_exists("quests.json"), "r") allquests=json.load(f) f.close() # Load story data f=open(get_path_if_exists("story.json"), "r") allstory=json.load(f) f.close() # Load world data f=open(get_path_if_exists("world.json"), "r") allworld=json.load(f) f.close() # Load tavern data f=open(get_path_if_exists("bar.json"), "r") alltaverns=json.load(f) f.close() # Load server news try: allnews=json.loads(requests.get("http://"+HOST+'/news.json', timeout=5.0).text) except: allnews=[] pass print "[OK] Basic functions loaded" tr_load=True return tr_load ############################################################################ # This is the JSON data formatter init python: import json def logindata(): global password # TODO: Obtain user id based on device return """{ "passwd": "%s", "version": "%s" }""" % (password, config.version) def recruitdata(t, a): return """{ "tavern": %d, "amount": %d }""" % (t, a) def questdata(q, p): return """{ "quest_id": %d, "party_id": %d }""" % (q, p) def battledata(u, s): return """{ "unit": %s, "sphere": %s }""" % (json.dumps(u), json.dumps(s)) def card_composite(cid, path): # We need to try to get rarity try: r=allunits[int(cid)]["rare"] except: r=4 # FIXME # We need to try to get the element try: e=allunits[int(cid)]["attribute"] print str(e) except: e=0 return Composite( (640, 960), (0, 0), "gfx/cards/bg.png", (0, 0), path, (0, 0), "gfx/cards/"+str(r)+".png", (0, 0), "gfx/cards/ele/"+str(e)+".png") # size,) # TODO: square_ and gfx/square/units # Converts regular image filepaths to displayables names and vice-versa def img_regex(name, reverse=False): if not reverse: # Extension is ommited, add them yourself! if name.startswith("unit_"): return "gfx/units/%s" % name.replace("unit_") elif name.startswith("mob_"): return "gfx/mobs/%s" % name.replace("mob_") elif name.startswith("dialog_"): return "gfx/dialog/%s" % name.replace("dialog_") elif name.startswith("bg "): return "gfx/bg/%s" % name.replace("bg ") elif name.startswith("summon_"): return "gfx/summons/%s" % name.replace("summon_") else: print("ERROR: Not a regular filename") return name else: if name.startswith("gfx/units/"): return "unit_%s" % name elif name.startswith("gfx/mobs/"): return "mob_%s" % name elif name.startswith("gfx/dialog/"): return "dialog_%s" % name elif name.startswith("gfx/bg/"): return "bg %s" % name elif name.startswith("gfx/summons/"): return "summon_%s" % name else: print("ERROR: Not a regular display name") return name # Loads a sound called VAL def get_sfx(val, ext): # Search for the sound show_img(val, False, ext=ext) valb=dl_search(persistent.allfiles, 0, val)[1] if valb == ERR_INVALID: print ("Invalid Sound: %s (%s)" % (path, val)) return "sfx/regnum.mp3" # FIXME return valb # Load sprite images: "unit" for file in renpy.list_files(): fn=file.replace('gfx/units/','').replace('/', ' ').replace('.png','') if file.startswith('gfx/units/'): if file.endswith('.png') or file.endswith('.webp'): name = "unit_"+fn #renpy.image(name, Image(file, yanchor=1.0)) renpy.image(name, card_composite(fn, file)) dl=dl_search(persistent.allfiles, 0, name) if dl is not ERR_INVALID: persistent.allfiles.append((name, file)) allfiles.append(name) continue continue # Load sprite images: "mob" for file in renpy.list_files(): fn=file.replace('gfx/mobs/','').replace('/', ' ').replace('.png','') if file.startswith('gfx/mobs/'): if file.endswith('.png') or file.endswith('.webp'): name = "mob_"+fn renpy.image(name, Image(file, yanchor=1.0)) if not name in persistent.allfiles: persistent.allfiles.append((name, file)) allfiles.append(name) continue continue # Load sprite images: "dialog" for file in renpy.list_files(): fn=file.replace('gfx/dialog/','').replace('/', ' ').replace('.png','').replace('.webp','') if file.startswith('gfx/dialog/'): if file.endswith('.png') or file.endswith('.webp'): name = "dialog_"+fn renpy.image(name, Image(file, yanchor=1.0)) dl=dl_search(persistent.allfiles, 0, name) if dl is not ERR_INVALID: persistent.allfiles.append((name, file)) allfiles.append(name) continue continue # Load background images: "bg" for file in renpy.list_files(): fn=file.replace('gfx/bg/','').replace('/', ' ').replace('.png','').replace('.webp','') if file.startswith('gfx/bg/'): if file.endswith('.png') or file.endswith('.webp'): name = "bg "+fn renpy.image(name, Frame(file, 0, 0)) dl=dl_search(persistent.allfiles, 0, name) if dl is not ERR_INVALID: persistent.allfiles.append((name, file)) allfiles.append(name) continue continue # Load summon images: "summon" for file in renpy.list_files(): fn=file.replace('gfx/summons/','').replace('/', ' ').replace('.png','').replace('.webp','') if file.startswith('gfx/summons/'): if file.endswith('.png') or file.endswith('.webp'): name = "summon_"+fn renpy.image(name, Image(file, yanchor=1.0)) dl=dl_search(persistent.allfiles, 0, name) if dl is not ERR_INVALID: persistent.allfiles.append((name, file)) allfiles.append(name) continue continue def star_write(am): i, st = 0, "" while i < am: i+=1 st+="★" return st # Overrides renpy.image() method def new_img(name, where): # d: Downloaded path if not isinstance(name, tuple): name = tuple(name.split()) #d = renpy.renpy.easy.displayable(where) if renpy.android: d=ExtraImage(where) else: d = renpy.renpy.easy.displayable(where) renpy.renpy.display.image.register_image(name, d) return # Retrieves Ren'Py displayable name associated to PATH def get_img(path): # Search for the image name val=img_regex(path, True) show_img(val, False) valb=dl_search(persistent.allfiles, 0, val)[1] if valb == ERR_INVALID: print ("Invalid Image: %s (%s)" % (path, val)) return "gfx/spinner.png" return valb # Overrides renpy.show() and renpy.image() methods # Missing: transient=False, munge_name=True def show_img(img, show=True, at_list=[ ], tag=None, zorder=None, behind=[ ], atl=None, what=None, layer=None, ext=".png"): global tr_loading # Image exists, display it if img in allfiles: if show: renpy.show(img, at_list=at_list, tag=tag, zorder=zorder, behind=behind, atl=atl, what=what, layer=layer) return # Have we downloaded this image previously? path=dl_search(persistent.allfiles, 0, img) print str(path) # Image doesn't exists, we must download it while (path == ERR_INVALID): tr_loading=True # Latest version converts these formats to WebP if ext in [".png", ".jpg", ".jpeg"]: ext=".webp" # Otherwise, preserve extension. if renpy.android: addr="extra_%s%s" % (img.replace(" ", "_"), ext) else: addr="extra/%s%s" % (img.replace(" ", "_"), ext) f=open(get_path(addr), "w") stdout("Downloading additional file: %s" % img.replace(" ", "_")) x=requests.get("http://%s:%d/%s?token=%s" % (HOST, UPDP, img.replace(" ", "_"), get_token())) # , timeout=8.0 → Need to handle sudden death if x.status_code == 200: f.write(x.content) f.close() # Android needs paths to be saved by full if renpy.android: addr=get_path(addr) path=((img, addr)) persistent.allfiles.append(path) else: try: retry=renpy.call_screen("confirm", "Error downloading file.\nError Code: %d\n\nRetry?" % x.status_code, Return(True), Return(False)) if not retry: if tag is None: tag=img path=None if show: renpy.show("spinner", at_list=at_list, tag=tag, zorder=zorder, behind=behind, atl=atl, what=what, layer=layer) # TODO Show error return # TODO: “Retry?” except: print("Failed, trying again") # Image exists, but wasn't loaded yet if (path != ERR_INVALID and path is not None): print "Detected not loaded image" # Valid Image Extensions: PNG, JPG, JPEG, GIF, WEBP if ext in [".png", ".jpg", ".jpeg", ".gif", ".webp"]: # Maybe it is an unit if img.startswith("unit_"): new_img(img, card_composite(img.replace("unit_", ""), path[1])) else: new_img(img, path[1]) stdout("registered image: "+path[1]) allfiles.append(img) if show: renpy.show(img, at_list=at_list, tag=tag, zorder=zorder, behind=behind, atl=atl, what=what, layer=layer) tr_loading=False return # Something went wrong stdout("show_img reached abnormal ending") return ########################################################## # Other Music MUSIC_BATTLE=RetString("sfx/bgm03.mp3") MUSIC_BOSS=RetString("sfx/bgm04.mp3") MUSIC_PARTY=RetString("sfx/bgm02.mp3") #MUSIC_PARTY=ExecuteOnCall(get_sfx, "sfx_bgm05", ".mp3")#"sfx/bgm05.mp3" MUSIC_VICTORY=RetString("sfx/bgm06.mp3") MUSIC_WORLDMAP=RetString("sfx/bgm02.mp3") #MUSIC_WORLDMAP=ExecuteOnCall(get_sfx, "sfx_bgm07", ".mp3")#"sfx/bgm07.mp3" MUSIC_PROLOGUE01=RetString("sfx/regnum.mp3") MUSIC_PROLOGUE02=RetString("sfx/prologue.mp3") MUSIC_PROLOGUE03=RetString("sfx/shining.mp3")