########################################################################################
# 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, uuid
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 True or 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 True or 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...")
# wsock already imported
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, tr_busy
global ws
# TODO: if tr_busy already true, wait until it is made false
tr_busy=True
tr_load=False
tr_val=""
stdout("Sending: %s" % packet)
try:
ws.send(get_token() + ";" + packet + ";" + args)
except:
return False
# TODO: Wait for onmsg
# TODO: ping packets
return True
def send_packet(packet, args=""):
global tr_load, tr_val, tr_busy
# TODO: if tr_busy already true, wait until it is made false
# 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")