################################################################################# # This file is part of Spheres. # Copyright (C) 2019 Jesusalva # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program 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 General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . ################################################################################# # Player Module (parties, inventory, and player data) from utils import (stdout, dl_search, allunits, date_from_sql, date_from_now, now, compress, cli_search, Player, ApTimer) from consts import (SQL_DELAY, SQL_SAVE_TIME, MAX_INV_SIZE, AP_REGEN_TIME_F, ERR_LOGIN, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY, ERR_BAD, ERR_OFF, ERR_ERR, CLIENTVERSION, UNSELLABLE, DOUBLE_GP, EXP_UP, ERR_DONE, ERR_OK, MAX_PARTIES, NO_PARTY, SUPEREVOMAT, EVO_MAT, AP_REGEN_TIME, INT_MAX, EXPTABLE, SQL_CLEAR) import sql import json, re, traceback from copy import copy from threading import Timer ####################################### Private methods # Internal function for ApTimer, DO NOT CAST manually # FIXME: Maybe find a better replacement for this def ap_updater(token): return # Internal function for SQL, DO NOT CAST manually def sql_routine(): global Player, SQLTimer #stdout("Now saving SQL data") for token in Player: stdout("[SQL] Now saving token \"%s\"" % token) stdout(str(Player[token])) clear(token, SQL_DELAY) SQLTimer=Timer(SQL_SAVE_TIME, sql_routine) SQLTimer.daemon=True SQLTimer.start() return # Attempts to place an item. Return inv index, -1 on error def inventoryplace(token): # Check for a free slot or return next index try: i=Player[token]["inv"].index(None) except: i=len(Player[token]["inv"]) # Return -1 if array exceeds the maximum size if (i > MAX_INV_SIZE): return -1 else: return i # Adds/Deletes AP from a user, manages the ApTimer def update_ap(token, newval): return # Function which list all unit id on party def party_dupcheck(token, pid): ar=[] for unt in Player[token]["party_%s" % pid]: ar.append(unt["unit_id"]) return ar # Function which list all unit id on inventory def inv_dupcheck(token, unique=True): ar=[] for unt in Player[token]["inv"]: if unt is None: if unique: continue else: ar.append(0) else: if unique: if unt["unit_id"] in ar: continue else: ar.append(unt["unit_id"]) return ar def clear(token, mask=SQL_CLEAR): # This function saves and clears a token ######################################### try: stdout("Saving user ID %d" % Player[token]["userid"]) except: stdout("ERROR: Token \"%s\" is not valid." % token) return ######################################### try: # Save inventory data to SQL sql.save_inv(token, mask) # Erase inventory if asked if mask & SQL_CLEAR: Player[token]["inv"]={} # Save party data to SQL sql.save_party(token) # Erase parties if needed if mask & SQL_CLEAR: Player[token]["party_1"]={} Player[token]["party_2"]={} Player[token]["party_3"]={} # Now save Player data to SQL sql.save_player(token, mask) # Erase Player Structure if needed if mask & SQL_CLEAR: del Player[token] # Stop the AP regeneration if we're clearing if mask & SQL_CLEAR: try: ApTimer[token].cancel() del ApTimer[token] except: pass except: stdout("SQL ERROR: FAILED TO CLEAR TOKEN %s" % token) if mask & SQL_CLEAR: del Player[token] if mask & SQL_CLEAR: try: ApTimer[token].cancel() del ApTimer[token] except: pass # TODO: Battle[token] return def exp_to_lvlup(level, unit=False): try: next_exp=EXPTABLE[level] if unit: next_exp=int(next_exp/1.5) except: stdout("Level %d is out of bounds" % (level)) next_exp=INT_MAX #stdout("nmult is %.2f, exp needed: %d" % (nmult, next_exp)) return next_exp # Check if player ranked up # Return: {"code": True/False, "ap": , "next": } def check_rank_up(token): rs={"code": False, "ap": 0} next_exp=exp_to_lvlup(Player[token]["level"]) if Player[token]["exp"] >= next_exp: # We leveled up! Prepare the bonuses rs["code"]=True # AP rules: Every 5 levels: +1 AP (always) if Player[token]["level"] % 5 == 0: rs["ap"]+=1 # AP rules: Every 10 levels: +1 AP (always) if Player[token]["level"] % 10 == 0: rs["ap"]+=1 # AP rules: Every level before the 20th: +1 AP (always_ if Player[token]["level"] < 20: rs["ap"]+=1 # Apply the level up Player[token]["exp"]-=next_exp Player[token]["level"]+=1 Player[token]["max_ap"]+=rs["ap"] Player[token]["max_exp"]=exp_to_lvlup(Player[token]["level"]) update_ap(token, Player[token]["max_ap"]-Player[token]["ap"]) # Sanitize Player[token]["exp"]=min(Player[token]["exp"], exp_to_lvlup(Player[token]["level"])) return rs # Conditional EXP attribution # Doesn't returns anything def grant_exp(token, inv_id, exp): # We need some data un=dl_search(allunits, "unit_id", Player[token]["inv"][inv_id]["unit_id"]) if un == "ERROR": stdout("WARNING, Invalid unit id on GEXP: %d" % (Player[token]["inv"][inv_id]["unit_id"])) return # Max level, do nothing if Player[token]["inv"][inv_id]["level"] >= un["max_level"]: return Player[token]["inv"][inv_id]["exp"]+=exp return # Same as check_rank_up but for units # Returns True/False def check_level_up(token, inv_id): global allunits # Prepare un, the variable from unit database un=dl_search(allunits, "unit_id", Player[token]["inv"][inv_id]["unit_id"]) if un == "ERROR": stdout("WARNING, Invalid unit id on CLU: %d" % (Player[token]["inv"][inv_id]["unit_id"])) return -1 # FIX if Player[token]["inv"][inv_id]["level"] > un["max_level"]: stdout("WARNING, Overlevelled unit (Lv %d/%d)" % (Player[token]["inv"][inv_id]["level"], un["max_level"])) Player[token]["inv"][inv_id]["exp"]=0 Player[token]["inv"][inv_id]["level"]=copy(un["max_level"]) # Max level reached (FIXME why it still gets exp?!) if Player[token]["inv"][inv_id]["level"] >= un["max_level"]: return 0 # Calculate exp needed to level up next_exp=exp_to_lvlup(Player[token]["inv"][inv_id]["level"], True) ret=0 while Player[token]["inv"][inv_id]["exp"] >= next_exp: ret += 1 Player[token]["inv"][inv_id]["exp"]-=next_exp Player[token]["inv"][inv_id]["level"]+=1 # No overflow! if Player[token]["inv"][inv_id]["level"] >= un["max_level"]: Player[token]["inv"][inv_id]["exp"]=0 Player[token]["inv"][inv_id]["level"]=copy(un["max_level"]) break next_exp=exp_to_lvlup(Player[token]["inv"][inv_id]["level"], True) return ret # Calculates sell price of an unit # After 10 stars, the sell price overflows 10,000 and gets messy # So I capped it at 10,000. Further stars will only raise level weight. def calc_sell_price(rarity, level): return min(10000, rarity**2*100)+(level*rarity) def daily_login(token): # Prepare dates from player last login, and from now # FIXME: "lastlogin" is updated in cycles # If players pass midnight connected --> BOOM # We should fill "lastlogin" at sql.load_player() only, instead of # auto-updating it dlcode=copy(ERR_LOGIN) stdout("Last login: %s" % str(Player[token]["lastlogin"])) # dlcode mask: (ERR_LOGIN meaning) # 5 - 0 - 0 - 0 # ID - Monthly - Monthly - Weekly d,m,y,w,H,M=date_from_sql(str(Player[token]["lastlogin"])) td,tm,ty,tw,tH,tM=date_from_now() # We don't really care with this bit of info del H, M, tH, tM # It's not the same: A reward is due if (d!=td or m!=tm or y!=ty): dlcode+=1000 # TODO: Weekly rewards if (w != tw) or (w == tw and (d != td or m != tm or y != ty)): # A weekly reward is due! dlcode+=tw if tw == MONDAY: Player[token]["crystals"]+=50 elif tw == TUESDAY: Player[token]["crystals"]+=50 elif tw == WEDNESDAY: Player[token]["crystals"]+=50 elif tw == THURSDAY: Player[token]["crystals"]+=50 elif tw == FRIDAY: Player[token]["crystals"]+=50 elif tw == SATURDAY: Player[token]["crystals"]+=50 elif tw == SUNDAY: Player[token]["crystals"]+=50 else: stdout("ERROR ERROR, UNKNOWN DAY OF WEEK %d" % tw) # TODO: Monthly rewards # TODO: Streak rewards # Finally, get rid of this data del Player[token]["lastlogin"] return dlcode ####################################### Public methods (Player/Client) def get_data(args, token): stdout("Data received") stdout("Now loading user ID: ```%s```" % str(args)) try: y=json.loads(args) passwd=y["passwd"] if not passwd.isalnum(): raise Exception("Illegal password") if len(passwd) not in [12, 16]: raise Exception("Illegal password") vs=str(y["version"]) except: return ERR_BAD # Version check if vs != CLIENTVERSION: return ERR_OFF # We have now loaded to memory from SQL # We definitely should use K-Line here target_uid=sql.load_player(token, passwd) # Maybe something went wrong, or password is invalid if (target_uid == ERR_BAD): return ERR_BAD if (target_uid == ERR_ERR): return ERR_ERR # Check if user is already logged in # If they are, cause a disconnection on old one and update tokens try: org_usr=cli_search(target_uid["userid"]) tk="0" if org_usr not in ["ERROR"]: stdout("Closing duplicate login from %s (token %s)" % (org_usr.address[0], org_usr.token)) tk=org_usr.token org_usr.close(status=1000, reason='Duplicated login') else: raise Exception("Not logged in") # TODO: ApTimer[tk] & Battle[tk] Player[token]=copy(Player[tk]) clear(tk) Player[token]["code"]=ERR_LOGIN Player[token]["token"]=token stdout("Player is already logged in") # Delete user id, send payload, create user id again uid=copy(Player[token]["userid"]) del Player[token]["userid"] paydata=compress(Player[token]) Player[token]["userid"]=copy(uid) del uid # Remove other temporary data del Player[token]["code"] del Player[token]["token"] return paydata except: #traceback.print_exc() pass # Create session Player[token] = target_uid # Complete additional data Player[token]["code"]=0 Player[token]["token"]=token # Give you offline AP and refresh to NOW, round down delta=(now()-Player[token]["aptime"])/AP_REGEN_TIME delta2=(now()-Player[token]["aptime"])%AP_REGEN_TIME Player[token]["ap"]=int(min(Player[token]["max_ap"], Player[token]["ap"]+delta)) Player[token]["aptime"]=now()-delta2 # Daily login rewards Player[token]["code"]=daily_login(token) # Include max_exp for the client Player[token]["max_exp"]=exp_to_lvlup(Player[token]["level"]) # Remove UID from packet and save to JSON uid=copy(Player[token]["userid"]) del Player[token]["userid"] sjson=compress(Player[token]) Player[token]["userid"]=copy(uid) # Clear temporary data, write internal big data fields del uid del Player[token]["code"] del Player[token]["token"] Player[token]["inv"]=sql.load_inv(token) Player[token]["party_1"]=sql.load_party(token, 1) Player[token]["party_2"]=sql.load_party(token, 2) Player[token]["party_3"]=sql.load_party(token, 3) # TODO: Load currency table # TODO: Load event table # TODO: Load world table # Logged in # {responseCode, token, status, gp, crystals, level, ap, max_ap, aptime } return sjson # This returns the player inventory. def get_inv(args, token): sjson=compress(Player[token]["inv"]) return sjson # This returns the player AP Data def ap_data(args, token): sjson=compress({"ap": Player[token]["ap"], "max_ap": Player[token]["max_ap"], "aptime": Player[token]["aptime"]}) return sjson # Sell units. Receives a single JSON array of inventory IDs. def sellunits(args, token): try: y=json.loads(args) gp=0 for tmp in y: tmpa=int(tmp) # Inventory size check (first check is not needed in a try loop...) if tmpa >= len(Player[token]["inv"]): raise Exception("Not in inventory") if Player[token]["inv"][tmpa] == None: stdout("None supplied (%d)" % tmpa) raise Exception("Not in inventory") # UNSELLABLE flag un=dl_search(allunits, "unit_id",Player[token]["inv"][tmpa]["unit_id"]) if un["flags"] & UNSELLABLE: stdout("This unit cannot be sold!") raise Exception("This unit cannot be sold!") # No duplicates if y.count(tmp) != 1: stdout("Duplication detected: %d" % (args.count(tmp))) raise Exception("Duplicate index detected") # Search in party stdout("PARTY LOOKUP IN PROGRESS") tmpb=dl_search(Player[token]["party_1"], "inv_id", tmpa) if tmpb == "ERROR": tmpb=dl_search(Player[token]["party_2"], "inv_id", tmpa) if tmpb == "ERROR": tmpb=dl_search(Player[token]["party_3"], "inv_id", tmpa) if tmpb != "ERROR": stdout("Unit is in party (%d)" % tmpb) raise Exception("Party members can't be sold") del tmpb # No point wasting time, we have "un" so sum GP as well am=calc_sell_price(un["rare"], Player[token]["inv"][tmpa]["level"]) if un["flags"] & DOUBLE_GP: am*=2 gp+=am del un, am del tmpa except: return ERR_BAD # Delete sold units and sum the GP for idx in y: Player[token]["inv"][idx]=None Player[token]["gp"]+=int(gp) sjson=compress('{"gp": %d, "profit": %d}' % (Player[token]["gp"], gp)) return sjson # Upgrade units. Receives a JSON array: [UNIT-TO-UPGRADE, MAT1, MAT2, MAT3...] # Based on invindex def upgrade(args, token): # Data validation (party members can't be used as material) try: y=json.loads(args) w=False for tmp in y: tmpa=int(tmp) # Inventory size check (first check is not needed in a try loop...) if tmpa >= len(Player[token]["inv"]): raise Exception("Not in inventory") if Player[token]["inv"][tmpa] == None: stdout("None supplied (%d)" % tmpa) raise Exception("Not in inventory") # First entry can be in the party, but must be uppable if not w: w=True un=dl_search(allunits, "unit_id",Player[token]["inv"][tmpa]["unit_id"]) stdout("Unit flags:") stdout("%s" % un["flags"]) #if un["flags"] & NO_LVLUP: # FIXME: Makes no sense now # stdout("This unit cannot level up!") # raise Exception("This unit cannot level up!") target_ele=un["attribute"] del un continue # Self fusion?? stdout("begin: starting") if y.count(tmp) != 1: stdout("Duplication detected: %d" % (args.count(tmp))) raise Exception("Duplicate index or self fusion detected") # Search in party stdout("LOOKUP IN PROGRESS") tmpb=dl_search(Player[token]["party_1"], "inv_id", tmpa) stdout("dl_search OK") if tmpb == "ERROR": tmpb=dl_search(Player[token]["party_2"], "inv_id", tmpa) if tmpb == "ERROR": tmpb=dl_search(Player[token]["party_3"], "inv_id", tmpa) if tmpb != "ERROR": stdout("Unit is in party (%d)" % tmpb) raise Exception("Party members can't be used as material") del tmpb del tmpa, w except: return ERR_BAD # Get "target" inv id target=y.pop(0) stdout("Target is (%d)" % target) # Define the experience you'll get by draining the relevant indexes xp=0 for idx in y: uxp=0 ud=Player[token]["inv"][idx]["unit_id"] un=dl_search(allunits, "unit_id", ud) try: r=un["rare"] f=un["flags"] e=un["attribute"] next_exp=exp_to_lvlup(Player[token]["inv"][idx]["level"], True) except: r=1 f=0 e=-1 stdout("ERROR, INVALID UNIT ID: %d" % ud) # Units with EXP_UP are always "max-level" if f & EXP_UP: lv=10+(r*10) else: lv=Player[token]["inv"][idx]["level"] uxp+=lv*20.0*((r+1)/2.0) uxp+=Player[token]["inv"][idx]["exp"]/next_exp*100.0*20.0 # Flags and same element bonus if f & EXP_UP: uxp*=1.5 if e == target_ele: uxp*=1.2 # Give the exp, and remove from inventory xp+=int(uxp) Player[token]["inv"][idx]=None # We now have the experience, so we grant it grant_exp(token, target, xp) r=check_level_up(token, target) sjson=compress(r) return sjson # args is the party ID def get_party(args, token): try: pid=int(args) if (pid > MAX_PARTIES): raise Exception("too many parties") except: return ERR_BAD sjson=compress(Player[token]["party_%d" % pid]) return sjson # TODO: Obviously this is also a WIP # {"party_id": pid, "formation": [p1, p2, p3, p4]} def set_party(args, token): # Standard checks try: y=json.loads(args) tmp=y["party_id"] pid=int(tmp) # Maximum party number if (pid > MAX_PARTIES): raise Exception("too many parties") # Only integers for ele in y["formation"]: tmp=int(ele) # If the unit is not valid (in inventory), this will EXPLODE un=dl_search(allunits, "unit_id", Player[token]["inv"][tmp]["unit_id"]) if un["flags"] & NO_PARTY: raise Exception("This unit cannot be part of a party!") del un except: return ERR_BAD # FIXME: We can't have duplicates Oh My :o #Player[token]["party_1"]=[] stdout("Request to edit party %d" % pid) # Check each request before appending for i, idx in enumerate(y["formation"]): stdout("Now checking (%d, %d)" % (i, idx)) # Empty the index in analysis Player[token]["party_%s" % pid][i]={"unit_id": 0, "inv_id": -1} # Ignored index if (idx < 0): continue # Duplicate checking! if (Player[token]["inv"][idx]["unit_id"] in party_dupcheck(token, pid)): return ERR_BAD # Retrieve inventory data and replace it in player tokens Player[token]["party_%s" % pid][i]["unit_id"]=Player[token]["inv"][idx]["unit_id"] Player[token]["party_%s" % pid][i]["inv_id"]=idx return ERR_DONE # Evolve units. Receives a JSON array: [UNIT-TO-EVOLVE, MAT1, MAT2] # Based on invindex def evolve(args, token): # Data validation (party members can't be used as material) try: y=json.loads(args) if len(y) != 3: return ERR_BAD w=False for tmp in y: tmpa=int(tmp) # Inventory size check (first check is not needed in a try loop...) if tmpa >= len(Player[token]["inv"]): raise Exception("Not in inventory") if Player[token]["inv"][tmpa] == None: stdout("None supplied (%d)" % tmpa) raise Exception("Not in inventory") # First entry can be in the party, but must be MAX LEVEL and level-able if not w: w=True un=dl_search(allunits, "unit_id",Player[token]["inv"][tmpa]["unit_id"]) stdout("Unit flags:") stdout("%s" % un["flags"]) #if un["flags"] & NO_LVLUP: # FIXME: Makes no sense now # stdout("This unit cannot level up!") # raise Exception("This unit cannot level up!") if un["max_level"] != Player[token]["inv"][tmpa]["level"]: stdout("raise: Level not yet maxed") raise Exception("Not yet max level!") target_ele=un["attribute"] target_id=un["unit_id"] target_rare=un["rare"] # Check if evolved version exists evolved=dl_search(allunits, "unit_id",Player[token]["inv"][tmpa]["unit_id"]+1) evolved_id=evolved["unit_id"] # Same as raise Exception if can't evolve del un continue # Self fusion?? stdout("begin: starting") if y.count(tmp) != 1: stdout("Duplication detected: %d" % (args.count(tmp))) raise Exception("Duplicate index or self fusion detected") # Search in party stdout("LOOKUP IN PROGRESS") tmpb=dl_search(Player[token]["party_1"], "inv_id", tmpa) stdout("dl_search OK") if tmpb == "ERROR": tmpb=dl_search(Player[token]["party_2"], "inv_id", tmpa) if tmpb == "ERROR": tmpb=dl_search(Player[token]["party_3"], "inv_id", tmpa) if tmpb != "ERROR": stdout("Unit is in party (%d)" % tmpb) raise Exception("Party members can't be used as material") # TODO: Check if it is suitable material (same rarity etc.) r=False tmpb=dl_search(allunits, "unit_id",Player[token]["inv"][tmpa]["unit_id"]) if tmpb["rare"] == target_rare: if tmpb["flags"] & SUPEREVOMAT: r=True elif tmpb["flags"] & EVO_MAT: if tmpb["attribute"] == target_ele: r=True elif tmpb["unit_id"] == target_id: r=True if not r: stdout("raise: Invalid evolution material") raise Exception("Invalid evolve material") del tmpb del tmpa, w except: return ERR_BAD # Get "target" inv id #target=y.pop(0) stdout("Target is (%d)" % target_id) # Evolve and remove material. Clear level/exp as well. Player[token]["inv"][y[0]]["unit_id"]+=1 Player[token]["inv"][y[0]]["level"]=0 Player[token]["inv"][y[0]]["exp"]=0 Player[token]["inv"][y[1]]=None Player[token]["inv"][y[2]]=None r=ERR_OK # TODO: Update party, NOT high priority try: tmpb=dl_search(Player[token]["party_1"], "inv_id", target_id) if tmpb != "ERROR": tmpb["unit_id"]+=1 tmpb=dl_search(Player[token]["party_2"], "inv_id", target_id) if tmpb != "ERROR": tmpb["unit_id"]+=1 tmpb=dl_search(Player[token]["party_3"], "inv_id", target_id) if tmpb != "ERROR": tmpb["unit_id"]+=1 # Now, do this cause a duplicate? If yes, unsocket it! tmpa=0 for tmpb in Player[token]["party_1"]: if tmpb["unit_id"] == evolved_id: tmpa+=1 if tmpa > 1: tmpb["unit_id"]=-1 tmpb["inv_id"]=-1 stdout("Unsocket") tmpa=0 for tmpb in Player[token]["party_2"]: if tmpb["unit_id"] == evolved_id: tmpa+=1 if tmpa > 1: tmpb["unit_id"]=-1 tmpb["inv_id"]=-1 stdout("Unsocket") tmpa=0 for tmpb in Player[token]["party_3"]: if tmpb["unit_id"] == evolved_id: tmpa+=1 if tmpa > 1: tmpb["unit_id"]=-1 tmpb["inv_id"]=-1 stdout("Unsocket") except: pass sjson=compress(r) return sjson # Creates a new account. Arguments: email def register(args, token): stdout("Request to register an account: %s" % str(args)) # https://emailregex.com/email-validation-summary/ - RFC allows more emails # But we don't want to risk compromising the database, and some are dump regex = '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$' try: y=json.loads(args) tmp=y["email"] mail=str(tmp) tmp=y["MyUID"] leni=len(tmp) MyUID=str(y["MyUID"]) # Check if email is valid with above regex if not re.search(regex,mail): if leni != 32 or mail != "": raise Exception("Not a valid email") mail="_%s@spheres.tmw2.org" % (MyUID) except: return ERR_BAD # TODO: "duplicate": all emails which difference consists on ponctuation #email=mail.replace(".", "").replace("_", "").replace("-", "").replace("+", "") stdout("Initial checks succeded") # Check if email is already registered check=sql.query_email(mail) if (check != ""): stdout("WARNING: Tried to register email \"%s\" (belongs to account %s)" % (mail, check)) return ERR_BAD # If email is an UID mail, clobber it check=sql.query_email("_%s@spheres.tmw2.org" % (MyUID)) if (check != ""): passwd=sql.clobber_email(check, "_%s@spheres.tmw2.org" % (MyUID), mail) stdout("INFO: Set a new email for \"%s\" (%s)" % (check, mail)) return compress({"userid": MyUID, "password": passwd}) stdout("Now registering account") # Register it data=sql.add_player(mail) if (data["userid"] <= 0): return ERR_ERR # From data, we have: userid, password return compress(data)