diff options
-rw-r--r-- | .gitlab-ci.yml | 3 | ||||
-rw-r--r-- | consts.py | 160 | ||||
-rwxr-xr-x | server.py | 7 | ||||
-rw-r--r-- | sql.py | 365 |
4 files changed, 534 insertions, 1 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f6cf829..78652de 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,6 +6,7 @@ stages: script: - apt-get update - apt-get -y -qq install python2.7 pyflakes + - cp pass.json.example pass.json - pyflakes . image: debian:buster @@ -25,7 +26,7 @@ pyflakes3: - apt-get -y -qq install python2.7 mariadb-server mariadb-client - make initdb - ./server.py ci - image: debian:unstable + image: debian:buster .skipci: stage: test diff --git a/consts.py b/consts.py new file mode 100644 index 0000000..6ea63f1 --- /dev/null +++ b/consts.py @@ -0,0 +1,160 @@ +######################################################################################## +# 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 +######################################################################################## +# Constants Module + +# Jobs +Job_Swordsman =1 +Job_Assassin =2 +Job_Mage =3 +Job_Archer =4 +Job_Gunner =5 + +# Elements +Ele_Fire =1 +Ele_Water =2 +Ele_Nature =3 +Ele_Light =4 +Ele_Shadow =5 + +# Gender +Sex_Male =0 +Sex_Female =1 + +# Limits +NO_LVLUP =1 +NO_PARTY =2 +EXP_UP =4 +EVO_MAT =8 +DOUBLE_GP =16 +UNSELLABLE =32 +SUPEREVOMAT =64 + +# Status +ST_TOWN =0 +ST_QUEST =1 + +# Spheres +SPH_NONE =0 +SPH_WIDEATTACK =1 +SPH_PIERCE =2 +SPH_ASSAULT =3 +SPH_HEAL =4 +SPH_HEALALL =5 +SPH_ATKUP =6 +SPH_DEFUP =7 + +# Status Conditions +SC_ATKUP =1 +SC_DEFUP =2 +SC_ATKDOWN =4 +SC_DEFDOWN =8 +SC_POISON =16 +SC_BLIND =32 +SC_STONED =64 + +# Battle action +ACT_NONE =0 +ACT_SPHERE =1 +ACT_SKILL =2 + +# Date Constants +# [0]- Day, [1]- Month, [2]- Year, [3]-Hour, [4]-Minute, [5]-Weekday +DATE_DAY =0 +DATE_MONTH =1 +DATE_YEAR =2 +DATE_WEEKDAY =3 +DATE_HOUR =4 +DATE_MINUTE =5 + +MONDAY =0 +TUESDAY =1 +WEDNESDAY =2 +THURSDAY =3 +FRIDAY =4 +SATURDAY =5 +SUNDAY =6 + +# Configuration (hardcoded!) +MAX_INV_SIZE =25 +MAX_PARTIES =3 +AP_REGEN_TIME =360 +AP_REGEN_TIME_F =360.0 +SQL_SAVE_TIME =300.0 +CONN_LIFETIME =1800 +CONN_CLEANUP =900.0 +SQL_PINGTIME =1200.0 +CLIENTVERSION ="2.0.6.18" + +# Hard coded loot (1,000~10,000) +CRYSTAL_MIN=1000 +CRYSTAL_MAX=2000 +EXPRATE_MIN=2001 +EXPRATE_MAX=4000 +GPPRATE_MIN=4001 +GPPRATE_MAX=6000 + +# Quick shortcuts +CRYSTAL_10 =1010 +CRYSTAL_20 =1020 +CRYSTAL_30 =1030 +CRYSTAL_50 =1050 +CRYSTAL_100=1100 +DOUBLE_EXP =3000 +TRIPLE_EXP =4000 +DOUBLE_GP =5000 +TRIPLE_GP =6000 + +# Error Handler +ERR_ERR ="500 Internal Server Error" # Maybe 418 I am a teapot +ERR_OFF ="401 Unauthorized" +ERR_BAD ="400 Bad Request" +ERR_DONE ="200" +#"You cannot perform this operation! Not enough Crystals" +ERR_INS =105 +ERR_FULL =106 +#"Operation complete" +ERR_OK =200 +ERR_LOGIN =5000 + +# SQL masks +SQL_NONE =0 +SQL_CLEAR =1 +SQL_DELAY =2 +SQL_FULL =4 + +# quest.json flags +SFLAG_NONE =0 # Normal battle +SFLAG_CLEARGEMS =1 # Give 100 gems upon first victory +SFLAG_DOUBLEGEMS =2 # Give 2x crystals (200 gems) upon first victory +SFLAG_SPECIAL =4 # This is a special quest - do not update quest field +SFLAG_FIRSTLOOT =8 # Always collect the first loot on first victory. +SFLAG_DOUBLEEXP =16 # Double EXP earned +SFLAG_DOUBLEGP =32 # Double GP earned + +# Skill types +SK_CLEAR_SC =1 +SK_SINGLE_DMG =2 +SK_MULTI_DMG =4 +SK_ATK_UP =8 +SK_DEF_UP =16 +SK_SINGLE_HEAL =32 +SK_MULTI_HEAL =64 +SK_RESSURECTION =128 + + @@ -1,10 +1,17 @@ #!/usr/bin/python3 +## Global Modules import threading, time, json#, ssl #from simple_websocket_server import WebSocketServer, WebSocket + +## Semi-local modules from websock import WebSocketServer, WebSocket #from endpoint import MainEndpoint + +#### Local Modules from utils import stdout as stdout +from consts import * +import sql ############################################################### # Configuration @@ -0,0 +1,365 @@ +######################################################################################## +# 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 +######################################################################################## +# SQL Module +import mysql.connector +import uuid, threading, json +from utils import dlist, Player, stdout, now +from consts import * + +############################################################### +# Configuration +f=open("pass.json", "r") +p=json.load(f) +f.close() + +SQLHOST=p["HOST"] +SQLUSER=p["SQLUSER"] +SQLPASS=p["SQLPASS"] +SQLDBID=p["SQLDB"] +clients = [] + +############################################################### +# Connect to database +db = mysql.connector.connect( + host=SQLHOST, + user=SQLUSER, + passwd=SQLPASS, + database=SQLDBID + ) + +####################################### Private methods +def save_player(token, mask=SQL_NONE): + global Player + w = db.cursor() + m="" + # Do "update delayed" even exist? + #if mask & SQL_DELAY: + # m+=" DELAYED" + + # Update player APTime before saving + Player[token]["aptime"]=now() + + #stdout(str(Player[token])) + sqlx="""UPDATE%s `player` SET + status = %d, quest = %d, gp = %d, crystals = %d, + exp = %d, level = %d, ap = %d, max_ap = %d, aptime = %d + WHERE `userid`=%d""" % (m, + Player[token]["status"], + Player[token]["quest"], + Player[token]["gp"], + Player[token]["crystals"], + Player[token]["exp"], + Player[token]["level"], + Player[token]["ap"], + Player[token]["max_ap"], + Player[token]["aptime"], + Player[token]["userid"]) + + stdout(sqlx) + try: + w.execute(sqlx) + except: + stdout("SQL.ERR: Error happened (save.player), commiting anyway due MyISAM...") + + #w.execute("INSERT INTO ....") # <- Not in save_player + #w.execute("REPLACE DELAYED....") # <- Not where you can't delete, but for inv + #w.execute("""INSERT INTO table (id, name, age) VALUES(1, "A", 19) ON DUPLICATE KEY UPDATE name="A", age=19""") + #w.execute("""INSERT INTO table (id, name, age) VALUES(1, "A", 19) ON DUPLICATE KEY UPDATE name=name, age=19;""") + db.commit() + w.close() + return ERR_OK + + +def load_inv(token): + uid=Player[token]["userid"] + inv=dlist() + + w = db.cursor(dictionary=True) + w.execute("SELECT * FROM `inventory` WHERE `userid`=%d" % (uid)) + r = w.fetchall() + for it in r: + inv[it["index"]]={"unit_id": it["unit_id"], + "level": it["unit_lv"], + "exp": it["unit_xp"]} + + w.close() + + return inv + + +def save_inv(token, mask=SQL_NONE, force=False): + uid=Player[token]["userid"] + inv=Player[token]["inv"] + fatalmsg="This error is fatal, we abort inventory saving!" + m="" + # FIXME: This is not a transactional database + if mask & SQL_DELAY: + m+=" DELAYED" + + w = db.cursor() + i=0 + revert=False + + # If force, we will commit whatever we have + if (force): + fatalmsg="" + + while i < len(inv): + it=inv[i] + # Blank items doesn't need saving, but may need deleting!! + if (it is None): + # Do we need to perhaps delete the ID? + w.execute("""DELETE FROM `inventory` + WHERE `userid` = %d AND `index` = %d""" + % (uid, i)) + stdout("SQL.INV: Delete where uid %d and index %d" % (uid, i)) + i+=1 + continue + + # Non-blank items, however, do need + try: + w.execute("""REPLACE%s INTO `inventory` + (`userid`, `index`, `unit_id`, `unit_lv`, `unit_xp`) + VALUES (%d, %d, %d, %d, %d)""" + % (m, uid, i, it["unit_id"], it["level"], it["exp"])) + except: + # Something went wrong! + print("ERROR Saving item index %d for player %d: %s\n%s" % (i, uid, str(it), fatalmsg)) + if not force: + revert=True + finally: + i+=1 + + # If something went wrong, don't save inventory + if revert: + #db.rollback() + stdout("SQL.ERR: Error happened, commiting anyway due MyISAM...") + + # We should now remove every item with an index higher than length + # In case the inventory shrink without None's taking the place... + w.execute("""DELETE FROM `inventory` + WHERE `userid` = %d AND `index` >= %d""" + % (uid, len(inv))) + stdout("SQL.INV: delete indexes >= %d, committing changes" % len(inv)) + db.commit() + + w.close() + + return inv + +def save_party(token, mask=SQL_NONE): + global Player + w = db.cursor() + m="" + # Do "update delayed" even exist? + #if mask & SQL_DELAY: + # m+=" DELAYED" + + i=1 + while i <= MAX_PARTIES: + try: + sqlx="""UPDATE%s `party` SET + member1_id = %d, member1_ix = %d, + member2_id = %d, member2_ix = %d, + member3_id = %d, member3_ix = %d, + member4_id = %d, member4_ix = %d + WHERE `userid`=%d AND `party_id`=%d""" % (m, + Player[token]["party_%d" % i][0]["unit_id"], + Player[token]["party_%d" % i][0]["inv_id"], + Player[token]["party_%d" % i][1]["unit_id"], + Player[token]["party_%d" % i][1]["inv_id"], + Player[token]["party_%d" % i][2]["unit_id"], + Player[token]["party_%d" % i][2]["inv_id"], + Player[token]["party_%d" % i][3]["unit_id"], + Player[token]["party_%d" % i][3]["inv_id"], + Player[token]["userid"], i) + + stdout(sqlx) + w.execute(sqlx) + except: + print("[SQL ERROR] Impossible to save party %d" % i) + db.rollback() + i+=1 + + db.commit() + w.close() + return ERR_OK + + +def load_party(token, pid): + uid=Player[token]["userid"] + pid=int(pid) + inv=[] + + w = db.cursor(dictionary=True) + w.execute("SELECT * FROM `party` WHERE `userid`=%d AND `party_id`=%d" % (uid, pid)) + r = w.fetchone() + j=0 + while j < 4: + j+=1 + try: + inv.append({"unit_id": r["member%d_id" % j], + "inv_id": r["member%d_ix" % j]}) + except TypeError: + # Maybe we should append an empty field instead? + inv.append({"unit_id": 0, + "inv_id": -1}) + except: + print("Error loading party (token: %s, ID: %d) (j: %d k: %d)" % (token, uid, j, pid)) + print("r is: %s" % str(r)) + return ERR_ERR + + w.close() + + return inv + + +def query_email(xmail): + w = db.cursor(dictionary=True) + w.execute("SELECT `userid` FROM `login` WHERE `email`='%s'" % (xmail)) + r = w.fetchall() + + c = "" + for it in r: + c+=str(it) + + w.close() + + return c + + +def add_player(xmail): + # TODO: Generate password using the whole alphabet. + # The original string have 32 letters and we're using only 12 + passwd=uuid.uuid4().hex[:12].upper() + + + w = db.cursor() + + # FIXME: Escaping email would be a good idea, but is not needed here. + # Why is it not needed? Because this is a private function called by + # player.register(), which already applies a regex filter and hard-fails + # in case the email is invalid. SQL Injections would result in an invalid + # email. + # BECAUSE THIS IS A PRIVATE FUNCTION, I've been negligent to check twice. + # Or to escape the string, just to be safe. + w.execute("""INSERT INTO `login` + (userpw, email) + VALUES ("%s", "%s")""" + % (passwd, + xmail)) + db.commit() + + # Retrieve the new userid + userid=w.lastrowid + + # Less relevant + # Prepare tutorial data + w.execute("""INSERT INTO `player` + (userid) + VALUES (%d)""" + % (userid)) + w.execute("""INSERT INTO `inventory` + (`userid`, `index`, `unit_id`) + VALUES (%d, 0, 10000000)""" + % (userid)) + w.execute("""INSERT INTO `party` + (userid, party_id, member1_id, member1_ix) + VALUES (%d, 1, 10000000, 0)""" + % (userid)) + w.execute("""INSERT INTO `party` + (userid, party_id) + VALUES (%d, 2)""" + % (userid)) + w.execute("""INSERT INTO `party` + (userid, party_id) + VALUES (%d, 3)""" + % (userid)) + db.commit() + w.close() + + bf={"userid": userid, "password": passwd} + return bf + + + +####################################### Public methods (Player/Client) +def load_player(token, password): + w = db.cursor(dictionary=True) + + # This is impossible + try: + if not password.isalnum(): + raise Exception("Idiotic password") + except: + w.close() + return ERR_ERR + + # FIXME: Escaping password would be a good idea, but is not needed here. + # Why is it not needed? Because this is a private function called by + # player.get_data(), which already applies an isalpnum filter and hard-fails + # in case the password is invalid. SQL Injections would result in an invalid + # password, which must be alphanumeric. + # BECAUSE THIS IS A PRIVATE FUNCTION, I've been negligent to check twice. + # Or to escape the string, just to be safe. + + # Validade userpw (the recovery password), return ERR_BAD if not found + w.execute("SELECT `userid` FROM `login` WHERE `userpw`='%s'" % (password)) + r = w.fetchone() + if r is None: + w.close() + return ERR_BAD + + # Select the user ID + uid=r["userid"] + + # Retrieve player data + w.execute("SELECT * FROM `player` WHERE `userid`=%d" % (uid)) + r = w.fetchone() + try: + tmp=r["status"] + except: + # User account does not exists, or something went wrong!! + w.close() + return ERR_ERR + + # Mark the player login AFTER we got the data + w.execute("UPDATE `player` SET lastlogin = CURRENT_TIMESTAMP WHERE `userid`=%d" % uid) + db.commit() + + # Send Player Structure (last login data is deleted on daily login handler) + w.close() + return r + +# sql.keep_alive() -> Pinger routine +def keep_alive(): + try: + db.ping(reconnect=True, attempts=10, delay=1) + except: + # SQL error + stdout("keep_alive: INTERNAL ERROR") + db.reconnect(attempts=12, delay=10) + sql_keep_alive=threading.Timer(SQL_PINGTIME, keep_alive) + sql_keep_alive.daemon=True + sql_keep_alive.start() + return + +# Begin sql.keep_alive() routine +keep_alive() + |