summaryrefslogblamecommitdiff
path: root/player.py
blob: 416e39e1d1d8e6095626c92eb20aeb81ddf6ea9c (plain) (tree)



















                                                                                        
                                                                             
                                               
                                                                            

                                                                              
                                                                                 
                                 
          
                          




















                                                                        
                                     





                                                              
















































































                                                                       



                                             






                                                             







































                                                             
                         

          
                                    
        
                                

                                      


                                                     
                                                                





                                                                       
                                                 


















                                                               
                                                                     
                                                                     
                  
                                                                                            








































                                                                                                                  
                                                                      









                                                                       
                                                                          



































































                                                                       
                                               
                             
                                               







                            











                                                                     
        


                                                
                                                                                                      
                            
                                                                 

                                            



                                        















                                                            
                              

            

                              






                                                        
                                                        
                                                                               
                                        


                                                       
                                                                              





                                            



                                                                 















                                                          
                            




                                                                             
                                    



                                        






                                                       



















































































































                                                                                       
                                                                           




































































































































































































































































                                                                                                 
########################################################################################
#     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
########################################################################################
# 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):
    global ApTimer, Player

    # TODO: What if token is no longer valid? (Maybe use queue instead?)
    Player[token]["ap"]+=1
    if (Player[token]["ap"] < Player[token]["max_ap"]):
        try:
            ApTimer[token].cancel()
            del ApTimer[token]
        except:
            pass
        ApTimer[token]=Timer(AP_REGEN_TIME_F, ap_updater, args=[token])
        ApTimer[token].daemon=True
        ApTimer[token].start()

    # Send APREFRESH packet to client
    try:
        usr=cli_search(Player[token]["userid"])
        usr.send_message("APREFRESH:%d" % Player[token]["ap"])
    except:
        traceback.print_exc()
        stdout("Unable to send AP Info for token %s" % 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):
    global ApTimer, Player

    running=False
    if (Player[token]["ap"] < Player[token]["max_ap"]):
        running=True

    # Update AP
    # TODO: Do we allow over-the-cap AP?
    Player[token]["ap"]+=newval

    # Handle the timer
    if (not running and Player[token]["ap"] < Player[token]["max_ap"]):
        ApTimer[token]=Timer(AP_REGEN_TIME_F, ap_updater, args=[token])
        ApTimer[token].daemon=True
        ApTimer[token].start()
    if (running and Player[token]["ap"] >= Player[token]["max_ap"]):
        # If it is not running/error: We don't care (very rare error)
        try:
            ApTimer[token].cancel()
        except:
            pass

    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": <ap_raised>, "next": <exp_needed>}
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"]=max(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) != 12:
            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"]=min(Player[token]["max_ap"], Player[token]["ap"]+delta)
    Player[token]["aptime"]=now()-delta2

    # This is only to start the timer if needed
    if (Player[token]["ap"] < Player[token]["max_ap"]):
        ApTimer[token]=Timer(AP_REGEN_TIME_F-delta2, ap_updater, args=[token])
        ApTimer[token].daemon=True
        ApTimer[token].start()

    # 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)
        # Check if email is valid with above regex
        if not re.search(regex,mail):
            raise Exception("Not a valid email")
    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

    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)