summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml3
-rw-r--r--consts.py160
-rwxr-xr-xserver.py7
-rw-r--r--sql.py365
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
+
+
diff --git a/server.py b/server.py
index f461b82..af694d7 100755
--- a/server.py
+++ b/server.py
@@ -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
diff --git a/sql.py b/sql.py
new file mode 100644
index 0000000..40ee446
--- /dev/null
+++ b/sql.py
@@ -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()
+