Browse Source

API v2 added:

 - upload_profile
 - upload_run_dimension
 - upload_run_dungeon
 - upload_run_scenario
 - upload_run_toa

A lot of testing still needed for different use cases.
Iñigo Valentin 6 năm trước cách đây
mục cha
commit
d2928213af

+ 1 - 0
.gitignore

@@ -7,6 +7,7 @@ data/*.sqlite
 .php_pid
 .php_pid
 *.log
 *.log
 doc/.tmp
 doc/.tmp
+application/API/v2/bin/__pycache__/
 
 
 #IDE / OS files
 #IDE / OS files
 .kateproject.d/
 .kateproject.d/

+ 49 - 0
application/API/v1/API_Controller.php

@@ -0,0 +1,49 @@
+<?php
+
+    /**
+     * v1 API Controller file.
+     *
+     * Provides a class to handle all posible API requests.
+     * 
+     * @category Constroller
+     */
+
+    /**
+     * v1 API controller.
+     *
+     * Handles every API request.
+     *
+     * @category Controller
+     */
+    class API_Controller {
+
+        /**
+         * Constructor.
+         *
+         * Handles every request, creating the required models and selecting the
+         * view.
+         *
+         * @param mixed[] $params GET parameters of the request.
+         */
+        public function __construct($params){
+
+            // API call: Use API controller
+            if (count($params) == 0){
+                require_once(__DIR__ . "main.php");
+            }
+            else{
+                $command = strtoupper$pars[1];
+                switch ($command){
+                    case "log-profile":
+                        require_once(__DIR__ . "log-profile/index.php");
+                        break;
+                    case "log-run":
+                        require_once(__DIR__ . "log-run/index.php");
+                        break;
+                    default:
+                        header("HTTP/1.1 404");
+                }
+            }
+        }
+    }
+?>

+ 0 - 0
application/bin/log-profile.py → application/API/v1/bin/log-profile.py


+ 0 - 0
application/bin/log-run.py → application/API/v1/bin/log-run.py


+ 54 - 0
application/API/v1/log-profile/index.php

@@ -0,0 +1,54 @@
+<?php
+    /**
+     * Profile uploader script.
+     *
+     * Exposes an API to save a run to the database.
+     * Reads post data and calls the log-profile.py script.
+     * It also saves the data to a JSON file in the da directory.
+     * Mandatory POST parameters are:
+     *  - data: Received JSON file after a run.
+     *  - key: User API key.
+     *
+     * @category API
+     */
+
+    $status = 200;
+    if (isset($_POST["data"]) && isset($_POST["key"])){
+        $data = $_POST["data"];
+        $key = $_POST["key"];
+        if (json_decode($data) === null){
+            error_log("ERROR: API v1 log-profile: Invalid JSON");
+            $status = 400;
+        }
+        else{
+            $json = json_decode($data);
+            $winfo = $json->{"wizard_info"};
+            $uname = $winfo->{"wizard_name"};
+            $uid = $winfo->{"wizard_id"};
+            $dtime = (new DateTime())->format('Y-m-dTH:i:s');
+            $fname = ($_SERVER["DOCUMENT_ROOT"] . "/../data/profile_" . $uid . "_" . $uname . "_" . $dtime . ".json");
+            file_put_contents($fname, $data);
+            $cmd =  __DIR__ . "../bin/log-profile.py " . $key . " " . $fname;
+            $out = [];
+            $ret = 0;
+            exec($cmd, $out, $ret);
+            if ($ret != 0){
+                error_log("ERROR: API v1 log-profile exited with status: " . $ret);
+                error_log("====================");
+                error_log("|| SCRIPT OUTPUT: ||");
+                error_log("===============================================================================");
+                foreach($out as $line){
+                    error_log("|| " . $line);
+                }
+                error_log("===============================================================================");
+                $status = 400;
+            }
+        }
+    }
+    else{
+        error_log("ERROR: API v1 log-profile: Invalid JSON");
+        $status = 400;
+    }
+    http_response_code($status);
+    return $status;
+?>

+ 46 - 0
application/API/v1/log-run/index.php

@@ -0,0 +1,46 @@
+<?php
+    /**
+     * Run logger script.
+     *
+     * Exposes an API to save a run to the database.
+     * Reads post data and calls the log-run.py script.
+     * Mandatory POST parameters are:
+     *  - data: Received JSON file after a run.
+     *  - key: User API key.
+     *
+     * @category API
+     */
+
+    $status = 200;
+    if (isset($_POST["data"]) && isset($_POST["key"])){
+        $data = $_POST["data"];
+        $key = $_POST["key"];
+        if (json_decode($data) === null){
+            error_log("ERROR: API v1 log-run: Invalid JSON");
+            $status = 400;
+        }
+        else{
+            $cmd = __DIR__ . "../bin/log-run.py " . $key . " " . escapeshellarg($data);
+            $out = [];
+            $ret = 0;
+            exec($cmd, $out, $ret);
+            if ($ret != 0){
+                error_log("ERROR: API v1 log-run exited with status: " . $ret);
+                error_log("====================");
+                error_log("|| SCRIPT OUTPUT: ||");
+                error_log("===============================================================================");
+                foreach($out as $line){
+                    error_log("|| " . $line);
+                }
+                error_log("===============================================================================");
+                $status = 400;
+            }
+        }
+    }
+    else{
+        error_log("ERROR: API v1 log-run: Invalid JSON");
+        $status = 400;
+    }
+    http_response_code($status);
+    return $status;
+?>

+ 60 - 0
application/API/v2/API_Controller.php

@@ -0,0 +1,60 @@
+<?php
+
+    /**
+     * v2 API Controller file.
+     *
+     * Provides a class to handle all posible API requests.
+     * 
+     * @category Constroller
+     */
+
+    /**
+     * v2 API controller.
+     *
+     * Handles every API request.
+     *
+     * @category Controller
+     */
+    class API_Controller {
+
+        /**
+         * Constructor.
+         *
+         * Handles every request, creating the required models and selecting the
+         * view.
+         *
+         * @param mixed[] $params GET parameters of the request.
+         */
+        public function __construct($params){
+            array_shift($params);
+            array_shift($params);
+            array_shift($params);
+            // API call: Use API controller
+            if (count($params) == 0){
+                require_once(__DIR__ . "main.php");
+            }
+            else{
+                $command = strtolower($params[0]);
+                switch ($command){
+                    case "upload_profile":
+                        require_once(__DIR__ . "/upload_profile.php");
+                        break;
+                    case "upload_run_dungeon":
+                        require_once(__DIR__ . "/upload_run_dungeon.php");
+                        break;
+                    case "upload_run_dimension":
+                        require_once(__DIR__ . "/upload_run_dimension.php");
+                        break;
+                    case "upload_run_scenario":
+                        require_once(__DIR__ . "/upload_run_scenario.php");
+                        break;
+                    case "upload_run_toa":
+                        require_once(__DIR__ . "/upload_run_toa.php");
+                        break;
+                    default:
+                        header("HTTP/1.1 404");
+                }
+            }
+        }
+    }
+?>

+ 382 - 0
application/API/v2/bin/MAPPING.py

@@ -0,0 +1,382 @@
+import json
+import math
+
+MAINSTAT = json.loads("""
+{
+  "mainstat": {
+    "1": {
+      "max": {
+        "1": 804,
+        "2": 1092,
+        "3": 1380,
+        "4": 1704,
+        "5": 2088,
+        "6": 2448
+      }
+    },
+    "2": {
+      "max": {
+        "1": 18,
+        "2": 20,
+        "3": 38,
+        "4": 43,
+        "5": 51,
+        "6": 63
+      }
+    },
+    "3": {
+      "max": {
+        "1": 54,
+        "2": 74,
+        "3": 93,
+        "4": 113,
+        "5": 135,
+        "6": 160
+      }
+    },
+    "4": {
+      "max": {
+        "1": 18,
+        "2": 20,
+        "3": 38,
+        "4": 43,
+        "5": 51,
+        "6": 63
+      }
+    },
+    "5": {
+      "max": {
+        "1": 54,
+        "2": 74,
+        "3": 93,
+        "4": 113,
+        "5": 135,
+        "6": 160
+      }
+    },
+    "6": {
+      "max": {
+        "1": 18,
+        "2": 20,
+        "3": 38,
+        "4": 43,
+        "5": 51,
+        "6": 63
+      }
+    },
+    "8": {
+      "max": {
+        "1": 18,
+        "2": 19,
+        "3": 25,
+        "4": 30,
+        "5": 39,
+        "6": 42
+      }
+    },
+    "9": {
+      "max": {
+        "1": 18,
+        "2": 20,
+        "3": 37,
+        "4": 41,
+        "5": 47,
+        "6": 58
+      }
+    },
+    "10": {
+      "max": {
+        "1": 20,
+        "2": 37,
+        "3": 43,
+        "4": 58,
+        "5": 65,
+        "6": 80
+      }
+    },
+    "11": {
+      "max": {
+        "1": 18,
+        "2": 20,
+        "3": 38,
+        "4": 44,
+        "5": 51,
+        "6": 64
+      }
+    },
+    "12": {
+      "max": {
+        "1": 18,
+        "2": 20,
+        "3": 38,
+        "4": 44,
+        "5": 51,
+        "6": 64
+      }
+    }
+  },
+  "substat": {
+    "1": {
+      "max": {
+        "1": 300,
+        "2": 525,
+        "3": 825,
+        "4": 1125,
+        "5": 1500,
+        "6": 1875
+      }
+    },
+    "2": {
+      "max": {
+        "1": 10,
+        "2": 15,
+        "3": 25,
+        "4": 30,
+        "5": 35,
+        "6": 40
+      }
+    },
+    "3": {
+      "max": {
+        "1": 20,
+        "2": 25,
+        "3": 40,
+        "4": 50,
+        "5": 75,
+        "6": 100
+      }
+    },
+    "4": {
+      "max": {
+        "1": 10,
+        "2": 15,
+        "3": 25,
+        "4": 30,
+        "5": 35,
+        "6": 40
+      }
+    },
+    "5": {
+      "max": {
+        "1": 20,
+        "2": 25,
+        "3": 40,
+        "4": 50,
+        "5": 75,
+        "6": 100
+      }
+    },
+    "6": {
+      "max": {
+        "1": 10,
+        "2": 15,
+        "3": 25,
+        "4": 30,
+        "5": 35,
+        "6": 40
+      }
+    },
+    "8": {
+      "max": {
+        "1": 5,
+        "2": 10,
+        "3": 15,
+        "4": 20,
+        "5": 25,
+        "6": 30
+      }
+    },
+    "9": {
+      "max": {
+        "1": 5,
+        "2": 10,
+        "3": 15,
+        "4": 20,
+        "5": 25,
+        "6": 30
+      }
+    },
+    "10": {
+      "max": {
+        "1": 10,
+        "2": 15,
+        "3": 20,
+        "4": 25,
+        "5": 25,
+        "6": 35
+      }
+    },
+    "11": {
+      "max": {
+        "1": 10,
+        "2": 15,
+        "3": 20,
+        "4": 25,
+        "5": 35,
+        "6": 40
+      }
+    },
+    "12": {
+      "max": {
+        "1": 10,
+        "2": 15,
+        "3": 20,
+        "4": 25,
+        "5": 35,
+        "6": 40
+      }
+    }
+  }
+}
+""")
+
+SUBSTAT = json.loads("""
+{
+  "substat": {
+    "1": {
+      "max": {
+        "1": 300,
+        "2": 525,
+        "3": 825,
+        "4": 1125,
+        "5": 1500,
+        "6": 1875
+      }
+    },
+    "2": {
+      "max": {
+        "1": 10,
+        "2": 15,
+        "3": 25,
+        "4": 30,
+        "5": 35,
+        "6": 40
+      }
+    },
+    "3": {
+      "max": {
+        "1": 20,
+        "2": 25,
+        "3": 40,
+        "4": 50,
+        "5": 75,
+        "6": 100
+      }
+    },
+    "4": {
+      "max": {
+        "1": 10,
+        "2": 15,
+        "3": 25,
+        "4": 30,
+        "5": 35,
+        "6": 40
+      }
+    },
+    "5": {
+      "max": {
+        "1": 20,
+        "2": 25,
+        "3": 40,
+        "4": 50,
+        "5": 75,
+        "6": 100
+      }
+    },
+    "6": {
+      "max": {
+        "1": 10,
+        "2": 15,
+        "3": 25,
+        "4": 30,
+        "5": 35,
+        "6": 40
+      }
+    },
+    "8": {
+      "max": {
+        "1": 5,
+        "2": 10,
+        "3": 15,
+        "4": 20,
+        "5": 25,
+        "6": 30
+      }
+    },
+    "9": {
+      "max": {
+        "1": 5,
+        "2": 10,
+        "3": 15,
+        "4": 20,
+        "5": 25,
+        "6": 30
+      }
+    },
+    "10": {
+      "max": {
+        "1": 10,
+        "2": 15,
+        "3": 20,
+        "4": 25,
+        "5": 25,
+        "6": 35
+      }
+    },
+    "11": {
+      "max": {
+        "1": 10,
+        "2": 15,
+        "3": 20,
+        "4": 25,
+        "5": 35,
+        "6": 40
+      }
+    },
+    "12": {
+      "max": {
+        "1": 10,
+        "2": 15,
+        "3": 20,
+        "4": 25,
+        "5": 35,
+        "6": 40
+      }
+    }
+  }
+}
+""")
+
+"""
+Calculates a rune current and max efficiency.
+
+:param rune: JSON woth rune info.
+:return: current, max efficiency.
+"""
+def calculate_efficiency(rune):
+    #getRuneEfficiency(rune, toFixed = 2) {
+    ratio = 0.0;
+
+    # Get actual stars (ancient are +10)
+    r_class = rune["class"]
+    if r_class > 10:
+        r_class -= 10
+    r_class = str(r_class)
+
+    # Main stat
+    ratio += MAINSTAT["mainstat"][str(rune["pri_eff"][0])]["max"][r_class] / MAINSTAT["mainstat"][str(rune["pri_eff"][0])]["max"]["6"]
+
+    # Innate stat
+    if (rune["prefix_eff"] and rune["prefix_eff"][0] > 0):
+      ratio += rune["prefix_eff"][1] / SUBSTAT["substat"][str(rune["prefix_eff"][0])]["max"]["6"];
+
+    # Sub stats
+    for substat in rune["sec_eff"]:
+        value = substat[1]
+        if "3" in substat and substat[3] > 0:
+            value += substat[3]
+        ratio += (value / SUBSTAT["substat"][str(substat[0])]["max"]["6"])
+
+    efficiency = (ratio / 2.8) * 100;
+
+    return ((ratio / 2.8) * 100), (efficiency + ((max(math.ceil((12 - rune["upgrade_curr"]) / 3.0), 0) * 0.2) / 2.8) * 100)

+ 1155 - 0
application/API/v2/bin/upload_profile.py

@@ -0,0 +1,1155 @@
+#!/usr/bin/python3
+
+import sys
+import os
+import sqlite3
+import json
+import math
+
+"""
+Reads the API KEY, that must be passed as first command line argument.
+
+:returns: Recovered API KEY.
+:raises Exception: Th KEY couldn't be red.
+"""
+def readKey():
+    try:
+        key = sys.argv[1]
+        return key
+    except Exception as e:
+        print("Error parsing API KEY: " + str(e))
+        raise
+
+"""
+Reads the JSON data, that must be passed as second command line argument.
+
+:returns: Recovered data, in JSON format.
+:raises Exception: The data couldn't be red or converted to JSON.
+"""
+def readData():
+    try:
+        #print(sys.argv[1])
+        #data = json.loads(sys.argv[1])
+        with open(sys.argv[2], 'r') as f:
+            content = f.read()
+        data = json.loads(content)
+        return data
+    except Exception as e:
+        print("Error parsing data: " + str(e))
+        raise
+
+"""
+Verifies that the API key matches the player data and that it exists in th DB.
+
+:param db: Connection to the database.
+:returns: Connection to the database.
+:param data: Data in json format.
+:param key: API KEY.
+:returns: True if key and player match, False otherwise.
+:raises IntegrityError: The queryes couldn't bre executed.
+"""
+def verifyKey(db, data, key):
+    print('Verifying KEY...')
+    status = False
+    try:
+        uid = data["wizard_info"]["wizard_id"]
+        cursor = db.cursor()
+        cursor.execute('SELECT count(uid) AS c FROM player WHERE uid = ? AND api_key = ?;', (uid, key))
+        if cursor.fetchone()[0] == 1:
+            status = True
+        cursor.close()
+    except sqlite3.IntegrityError as e:
+        print("Error executing statement: " + str(e))
+        raise
+    return status
+
+"""
+Opens the database file and cleans the user tables.
+
+It also disables referencial integrity.
+
+:param name: The path to the sqlite database.
+:returns: Connection to the database.
+:raises IntegrityError: The queryes couldn't bre executed.
+:raises IOError: The sqlite file couldn't be created.
+"""
+def openDatabase():
+    kdb = os.path.dirname(os.path.realpath(sys.argv[0])) + '/../../../sw.sqlite'
+    udb = os.path.dirname(os.path.realpath(sys.argv[0])) + '/../../../../data/sw.sqlite'
+    print('Configuring database...')
+    try:
+        db = sqlite3.connect(kdb)
+        cursor = db.cursor()
+        cursor.execute("attach '" + udb + "' as data;")
+        cursor.close()
+        return db
+    except sqlite3.IntegrityError as e:
+        print("Error executing statement: " + str(e))
+        raise
+    except IOError as e:
+        print("I/O Error creating database " + name + ": " + str(e))
+        raise
+
+"""
+Clears the user tables.
+
+It also disables referencial integrity.
+
+:param db: Sqlite database connection.
+:param data: Data in json format.
+:raises IntegrityError: The queryes couldn't bre executed.
+"""
+def clearData(db, data):
+    print('Clearing previous data...')
+    try:
+        uid = str(data["wizard_info"]["wizard_id"])
+        cursor = db.cursor()
+        cursor.execute('PRAGMA foreign_keys = OFF;')
+        cursor.execute('DELETE FROM scenario WHERE uid = ?', [uid])
+        cursor.execute('DELETE FROM defense WHERE uid = ?', [uid])
+        cursor.execute('DELETE FROM unit_skill WHERE unit IN (SELECT id FROM unit WHERE uid = ?)', [uid])
+        cursor.execute('DELETE FROM unit WHERE uid = ?', [uid])
+        cursor.execute('DELETE FROM rune WHERE uid = ?', [uid])
+        cursor.execute('DELETE FROM building WHERE uid = ?', [uid])
+        cursor.execute('DELETE FROM decoration WHERE uid = ?', [uid])
+        cursor.execute('DELETE FROM inventory WHERE uid = ?', [uid])
+        cursor.execute('DELETE FROM summon_special') # For every player, will be reloaded.
+        cursor.execute('DELETE FROM rune_craft WHERE uid = ?', [uid])
+        # 'guild' and 'guild_member' tables are cleared before inserting.
+        db.commit()
+        cursor.close();
+    except sqlite3.IntegrityError as e:
+        print("Error executing statement: " + str(e))
+        raise
+    return
+
+
+"""
+Closes the database.
+
+Before doing so, it also disables referencial integrity.
+
+:param db: Connection to the database.
+:returns: Connection to the database.
+:raises IntegrityError: The queryes couldn't bre executed.
+"""
+def closeDatabase(name):
+    print('Closing database connection...')
+    try:
+        cursor = db.cursor()
+        cursor.execute('PRAGMA foreign_keys = ON;')
+        cursor.close()
+        db.commit()
+        db.close()
+    except sqlite3.IntegrityError as e:
+        print("Error executing statement: " + str(e))
+        raise
+
+"""
+Inserts a row into the database.
+
+:param db: Connection to the database.
+:param table: Name of the table to insert into.
+:param values: List of values to insert.
+:raises IntegrityError: The insert query was unsuccesfull.
+"""
+def insert(db, table, values):
+    cursor = db.cursor()
+    db.execute('PRAGMA foreign_keys = OFF;')
+    placeholders = ''
+    for x in range(0, len(values)):
+        placeholders = placeholders + '?, '
+    placeholders = placeholders[:len(placeholders) - 2]
+    query = 'INSERT INTO ' + table + ' VALUES (' + placeholders + ')'
+    try:
+        cursor.execute(query, values)
+    except sqlite3.IntegrityError as e:
+        print('Error Inserting into ' + table + ' with values (' + str(values) + '): ' + str(e))
+        raise
+    cursor.close;
+
+"""
+Parses player (table player).
+
+:param db: Sqlite database connection.
+:param data: Data in json format.
+"""
+def parsePlayer(db, data):
+    print("Parsing player...")
+    cursor = db.cursor()
+    uid = data["wizard_info"]["wizard_id"]
+    name = data["wizard_info"]["wizard_name"]
+    # TODO: Generate data
+    mail = data["wizard_info"]["wizard_name"]
+    password = data["wizard_info"]["wizard_name"]
+    api_key = data["wizard_info"]["wizard_name"]
+    mana = data["wizard_info"]["wizard_mana"]
+    crystal = data["wizard_info"]["wizard_crystal"]
+    country = data["wizard_info"]["wizard_last_country"]
+    lang = data["wizard_info"]["wizard_last_lang"]
+    level = data["wizard_info"]["wizard_level"]
+    experience = data["wizard_info"]["experience"]
+    energy = data["wizard_info"]["wizard_energy"]
+    energy_max = data["wizard_info"]["energy_max"]
+    energy_per_min = data["wizard_info"]["energy_per_min"]
+    arena_energy = data["wizard_info"]["arena_energy"]
+    arena_energy_max = data["wizard_info"]["arena_energy_max"]
+    rep = data["wizard_info"]["rep_unit_id"]
+    social_point = data["wizard_info"]["social_point_current"]
+    honor_point = data["wizard_info"]["honor_point"]
+    guild_point = data["wizard_info"]["guild_point"]
+    darkportal_energy = data["wizard_info"]["darkportal_energy"]
+    darkportal_energy_max = data["wizard_info"]["darkportal_energy_max"]
+    dimension_energy = data["dimension_hole_info"]["energy"]
+    dimension_energy_max = data["dimension_hole_info"]["energy_max"]
+    costume_point = data["wizard_info"]["costume_point"]
+    costume_point_max = data["wizard_info"]["costume_point_max"]
+    honor_medal = data["wizard_info"]["honor_medal"]
+    honor_mark = data["wizard_info"]["honor_mark"]
+    event_coin = data["wizard_info"]["event_coin"]
+    storage_slots = data["unit_depository_slots"]["number"]
+    island_upgrade = 0
+    for island in data["island_info"]:
+        if island["id"] <= 7 and island["open"] == 1:
+            island_upgrade = island["id"]
+    query = '''
+        UPDATE player
+        SET mana = ?,
+            crystal = ?,
+            country = ?,
+            lang = ?,
+            level = ?,
+            experience = ?,
+            energy = ?,
+            energy_max = ?,
+            energy_per_min = ?,
+            arena_energy = ?,
+            arena_energy_max = ?,
+            rep = ?,
+            social_point = ?,
+            honor_point = ?,
+            guild_point = ?,
+            darkportal_energy = ?,
+            darkportal_energy_max = ?,
+            dimension_energy = ?,
+            dimension_energy_max = ?,
+            costume_point = ?,
+            costume_point_max = ?,
+            honor_medal = ?,
+            honor_medal = ?,
+            event_coin = ?,
+            storage_slots = ?,
+            island = ?
+        WHERE uid = ?;
+    '''
+    values = (
+        mana,                 crystal,               country,
+        lang,                 level,                 experience,
+        energy,               energy_max,            energy_per_min,
+        arena_energy,         arena_energy_max,      rep,
+        social_point,         honor_point,           guild_point,
+        darkportal_energy,    darkportal_energy_max, dimension_energy,
+        dimension_energy_max, costume_point,         costume_point_max,
+        honor_medal,          honor_medal,           event_coin,
+        storage_slots,        island_upgrade,        uid
+    )
+    try:
+        cursor.execute(query, values)
+        db.commit()
+    except sqlite3.IntegrityError as e:
+        print('Error updating player table with: ' + str(query) + ' <== ' + str(values) + ' || Error message:' +  str(e))
+        raise
+
+"""
+Parses scenarios (table scenario).
+
+:param db: Sqlite database connection.
+:param data: Data in json format.
+"""
+def parseScenarios(db, data):
+    print("Parsing scenarios...")
+    uid = data["wizard_info"]["wizard_id"]
+    for sce in data["scenario_list"]:
+        region = sce["region_id"]
+        difficulty = sce["difficulty"]
+        cleared = sce["cleared"]
+        max_cleared = 0
+        for stage in sce["stage_list"]:
+            if stage["cleared"] == 1:
+                max_cleared = stage["stage_no"]
+        insert(db, "scenario", (uid, region, difficulty, cleared, max_cleared))
+    db.commit()
+
+"""
+Parses arena defense units (table defense).
+
+:param db: Sqlite database connection.
+:param data: Data in json format.
+"""
+def parseDefense(db, data):
+    print("Parsing arena defense...")
+    uid = data["wizard_info"]["wizard_id"]
+    for defense in data["defense_unit_list"]:
+        unit = defense["unit_id"]
+        position = defense["pos_id"]
+        insert(db, "defense", (uid, unit, position))
+    db.commit()
+
+"""
+Parses buildings (table building).
+
+:param db: Sqlite database connection.
+:param data: Data in json format.
+"""
+def parseBuildings(db, data):
+    print("Parsing buildings...")
+    uid = data["wizard_info"]["wizard_id"]
+    for bui in data["building_list"]:
+        id = bui["building_id"]
+        building = bui["building_master_id"]
+        gain = bui["gain_per_hour"]
+        insert(db, "building", (uid, id, building, gain))
+    db.commit()
+
+"""
+Parses decoration buildings (table dcoration).
+
+:param db: Sqlite database connection.
+:param data: Data in json format.
+"""
+def parseDecorations(db, data):
+    print("Parsing decorations...")
+    uid = data["wizard_info"]["wizard_id"]
+    for bui in data["deco_list"]:
+        id = bui["deco_id"]
+        building = bui["master_id"]
+        level = bui["level"]
+        insert(db, "decoration", (uid, id, building, level))
+    db.commit()
+
+"""
+Parses unit data (tables unit, unit_skill).
+
+:param db: Sqlite database connection.
+:param data: Data in json format.
+"""
+def parseUnits(db, data):
+    print("Parsing monsters...")
+    uid = data["wizard_info"]["wizard_id"]
+    lock_list = data["unit_lock_list"]
+    for mon in data["unit_list"]:
+        id = mon["unit_id"]
+        building = mon["building_id"]
+        monster = mon["unit_master_id"]
+        level = mon["unit_level"]
+        stars = mon["class"]
+        hp = mon["con"] * 15 # Always x15
+        attack = mon["atk"]
+        defense = mon["def"]
+        speed = mon["spd"]
+        crit_rate = mon["critical_rate"]
+        crit_damage = mon["critical_damage"]
+        resistance = mon["resist"]
+        accuracy = mon["accuracy"]
+        experience = mon["experience"]
+        exp_gained = mon["exp_gained"]
+        exp_gain_rate = mon["exp_gain_rate"]
+        costume = mon["costume_master_id"]
+        source = mon["source"]
+        create_time = mon["create_time"]
+        homunculus_name = mon["homunculus_name"]
+        lock = 0
+        if id in lock_list:
+            lock = 1
+        insert(db, "unit", (uid, id, building, monster, level, stars, hp, attack, defense, speed, crit_rate, crit_damage, resistance, accuracy, experience, exp_gained, exp_gain_rate, costume, source, create_time, homunculus_name, lock))
+        for skill in mon["skills"]:
+            skill_id = skill[0]
+            skill_level = skill[1]
+            insert(db, "unit_skill", (id, skill_id, skill_level))
+    db.commit()
+
+"""
+Parses inventory data (table inventory).
+
+:param db: Sqlite database connection.
+:param data: Data in json format.
+"""
+def parseInventory(db, data):
+    print("Parsing inventory...")
+    uid = data["wizard_info"]["wizard_id"]
+    for item in data["inventory_info"]:
+        id = item["item_master_id"]
+        type = item["item_master_type"]
+        amount = item["item_quantity"]
+        insert(db, "inventory", (uid, id, type, amount))
+    # Fix rune craft item type.
+    cursor = db.cursor()
+    cursor.execute("UPDATE inventory SET type = 27 WHERE type = 29 AND id IN (2001, 4001, 4002, 4003, 9001, 9002, 9003, 8001);")
+    cursor.close()
+
+"""
+Parses summon stone list (table summon_special).
+
+:param db: Sqlite database connection.
+:param data: Data in json format.
+"""
+def parseSummonSpecial(db, data):
+    print("Parsing Summon stone monster list...")
+    uid = data["wizard_info"]["wizard_id"]
+    for unit in data["summon_special_info"]["this"]:
+        insert(db, "summon_special", (0, unit))
+    for unit in data["summon_special_info"]["next"]:
+        insert(db, "summon_special", (1, unit))
+    for unit in data["summon_special_info"]["third"]:
+        insert(db, "summon_special", (2, unit))
+    for unit in data["summon_special_info"]["fourth"]:
+        insert(db, "summon_special", (3, unit))
+
+"""
+Parses rune data (table rune).
+
+:param db: Sqlite database connection.
+:param data: Data in json format.
+"""
+def parseRunes(db, data):
+    print("Parsing runes...")
+    STAT_HP = 1
+    STAT_HP_PCT = 2
+    STAT_ATK = 3
+    STAT_ATK_PCT = 4
+    STAT_DEF = 5
+    STAT_DEF_PCT = 6
+    STAT_SPD = 8
+    STAT_CRIT_RATE_PCT = 9
+    STAT_CRIT_DMG_PCT = 10
+    STAT_RESIST_PCT = 11
+    STAT_ACCURACY_PCT = 12
+    MAIN_STAT_VALUES = {
+        # [stat][stars][level]: value
+        STAT_HP: {
+            1: [40, 85, 130, 175, 220, 265, 310, 355, 400, 445, 490, 535, 580, 625, 670, 804],
+            2: [70, 130, 190, 250, 310, 370, 430, 490, 550, 610, 670, 730, 790, 850, 910, 1092],
+            3: [100, 175, 250, 325, 400, 475, 550, 625, 700, 775, 850, 925, 1000, 1075, 1150, 1380],
+            4: [160, 250, 340, 430, 520, 610, 700, 790, 880, 970, 1060, 1150, 1240, 1330, 1420, 1704],
+            5: [270, 375, 480, 585, 690, 795, 900, 1005, 1110, 1215, 1320, 1425, 1530, 1635, 1740, 2088],
+            6: [360, 480, 600, 720, 840, 960, 1080, 1200, 1320, 1440, 1560, 1680, 1800, 1920, 2040, 2448],
+        },
+        STAT_HP_PCT: {
+            1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 18],
+            2: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 19],
+            3: [4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 38],
+            4: [5, 7, 9, 11, 13, 16, 18, 20, 22, 24, 27, 29, 31, 33, 36, 43],
+            5: [8, 10, 12, 15, 17, 20, 22, 24, 27, 29, 32, 34, 37, 40, 43, 51],
+            6: [11, 14, 17, 20, 23, 26, 29, 32, 35, 38, 41, 44, 47, 50, 53, 63],
+        },
+        STAT_ATK: {
+            1: [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 54],
+            2: [5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 73],
+            3: [7, 12, 17, 22, 27, 32, 37, 42, 47, 52, 57, 62, 67, 72, 77, 92],
+            4: [10, 16, 22, 28, 34, 40, 46, 52, 58, 64, 70, 76, 82, 88, 94, 112],
+            5: [15, 22, 29, 36, 43, 50, 57, 64, 71, 78, 85, 92, 99, 106, 113, 135],
+            6: [22, 30, 38, 46, 54, 62, 70, 78, 86, 94, 102, 110, 118, 126, 134, 160],
+        },
+        STAT_ATK_PCT: {
+            1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 18],
+            2: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 19],
+            3: [4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 38],
+            4: [5, 7, 9, 11, 13, 16, 18, 20, 22, 24, 27, 29, 31, 33, 36, 43],
+            5: [8, 10, 12, 15, 17, 20, 22, 24, 27, 29, 32, 34, 37, 40, 43, 51],
+            6: [11, 14, 17, 20, 23, 26, 29, 32, 35, 38, 41, 44, 47, 50, 53, 63],
+        },
+        STAT_DEF: {
+            1: [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 54],
+            2: [5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 73],
+            3: [7, 12, 17, 22, 27, 32, 37, 42, 47, 52, 57, 62, 67, 72, 77, 92],
+            4: [10, 16, 22, 28, 34, 40, 46, 52, 58, 64, 70, 76, 82, 88, 94, 112],
+            5: [15, 22, 29, 36, 43, 50, 57, 64, 71, 78, 85, 92, 99, 106, 113, 135],
+            6: [22, 30, 38, 46, 54, 62, 70, 78, 86, 94, 102, 110, 118, 126, 134, 160],
+        },
+        STAT_DEF_PCT: {
+            1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 18],
+            2: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 19],
+            3: [4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 38],
+            4: [5, 7, 9, 11, 13, 16, 18, 20, 22, 24, 27, 29, 31, 33, 36, 43],
+            5: [8, 10, 12, 15, 17, 20, 22, 24, 27, 29, 32, 34, 37, 40, 43, 51],
+            6: [11, 14, 17, 20, 23, 26, 29, 32, 35, 38, 41, 44, 47, 50, 53, 63],
+        },
+        STAT_SPD: {
+            1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 18],
+            2: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 19],
+            3: [3, 4, 5, 6, 8, 9, 10, 12, 13, 14, 16, 17, 18, 19, 21, 25],
+            4: [4, 5, 7, 8, 10, 11, 13, 14, 16, 17, 19, 20, 22, 23, 25, 30],
+            5: [5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 39],
+            6: [7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 42],
+        },
+        STAT_CRIT_RATE_PCT: {
+            1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 18],
+            2: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 19],
+            3: [3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 37],
+            4: [4, 6, 8, 11, 13, 15, 17, 19, 22, 24, 26, 28, 30, 33, 35, 41],
+            5: [5, 7, 10, 12, 15, 17, 19, 22, 24, 27, 29, 31, 34, 36, 39, 47],
+            6: [7, 10, 13, 16, 19, 22, 25, 28, 31, 34, 37, 40, 43, 46, 49, 58],
+        },
+        STAT_CRIT_DMG_PCT: {
+            1: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 19],
+            2: [3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 37],
+            3: [4, 6, 9, 11, 13, 16, 18, 20, 22, 25, 27, 29, 32, 34, 36, 43],
+            4: [6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 57],
+            5: [8, 11, 15, 18, 21, 25, 28, 31, 34, 38, 41, 44, 48, 51, 54, 65],
+            6: [11, 15, 19, 23, 27, 31, 35, 39, 43, 47, 51, 55, 59, 63, 67, 80],
+        },
+        STAT_RESIST_PCT: {
+            1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 18],
+            2: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 19],
+            3: [4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 38],
+            4: [6, 8, 10, 13, 15, 17, 19, 21, 24, 26, 28, 30, 32, 35, 37, 44],
+            5: [9, 11, 14, 16, 19, 21, 23, 26, 28, 31, 33, 35, 38, 40, 43, 51],
+            6: [12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 64],
+        },
+        STAT_ACCURACY_PCT: {
+            1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 18],
+            2: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 19],
+            3: [4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 38],
+            4: [6, 8, 10, 13, 15, 17, 19, 21, 24, 26, 28, 30, 32, 35, 37, 44],
+            5: [9, 11, 14, 16, 19, 21, 23, 26, 28, 31, 33, 35, 38, 40, 43, 51],
+            6: [12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 64],
+        },
+    }
+    SUBSTAT_INCREMENTS = {
+        # [stat][stars]: value
+        # Max possible substat value can be found by multiplying by 5
+        STAT_HP: {
+            1: 60,
+            2: 105,
+            3: 165,
+            4: 225,
+            5: 300,
+            6: 375,
+        },
+        STAT_HP_PCT: {
+            1: 2,
+            2: 3,
+            3: 5,
+            4: 6,
+            5: 7,
+            6: 8,
+        },
+        STAT_ATK: {
+            1: 4,
+            2: 5,
+            3: 8,
+            4: 10,
+            5: 15,
+            6: 20,
+        },
+        STAT_ATK_PCT: {
+            1: 2,
+            2: 3,
+            3: 5,
+            4: 6,
+            5: 7,
+            6: 8,
+        },
+        STAT_DEF: {
+            1: 4,
+            2: 5,
+            3: 8,
+            4: 10,
+            5: 15,
+            6: 20,
+        },
+        STAT_DEF_PCT: {
+            1: 2,
+            2: 3,
+            3: 5,
+            4: 6,
+            5: 7,
+            6: 8,
+        },
+        STAT_SPD: {
+            1: 1,
+            2: 2,
+            3: 3,
+            4: 4,
+            5: 5,
+            6: 6,
+        },
+        STAT_CRIT_RATE_PCT: {
+            1: 1,
+            2: 2,
+            3: 3,
+            4: 4,
+            5: 5,
+            6: 6,
+        },
+        STAT_CRIT_DMG_PCT: {
+            1: 2,
+            2: 3,
+            3: 4,
+            4: 5,
+            5: 6,
+            6: 7,
+        },
+        STAT_RESIST_PCT: {
+            1: 2,
+            2: 3,
+            3: 5,
+            4: 6,
+            5: 7,
+            6: 8,
+        },
+        STAT_ACCURACY_PCT: {
+            1: 2,
+            2: 3,
+            3: 5,
+            4: 6,
+            5: 7,
+            6: 8,
+        },
+    }
+    uid = data["wizard_info"]["wizard_id"]
+    # Unequipped runes
+    for rune in data["runes"]:
+        id = rune["rune_id"]
+        assigned_to = rune["occupied_id"]
+        if assigned_to == 0:
+            assigned_to = None
+        runeset = rune["set_id"]
+        slot = rune["slot_no"]
+        stars = rune["class"]
+        ancient = 0
+        level = rune["upgrade_curr"]
+        original_quality = rune["extra"]
+        value = rune["sell_value"]
+        if stars > 10:
+            ancient = 1
+            stars = stars - 10
+            original_quality = original_quality - 10
+        if level >= 12:
+            quality = 5
+        elif level >= 9:
+            quality = 4
+        elif level >= 6:
+            quality = 3
+        elif level >= 3:
+            quality = 2
+        else:
+            quality = 1
+        if original_quality > quality:
+            quality = original_quality
+        efficiency = 0
+        max_efficiency = 0
+        main_stat = rune["pri_eff"][0]
+        main_stat_value = rune["pri_eff"][1]
+        if rune["prefix_eff"][0] > 0:
+            innate_stat = rune["prefix_eff"][0]
+            innate_stat_value = rune["prefix_eff"][1]
+        else:
+            innate_stat = None
+            innate_stat_value = 0
+        if len(rune["sec_eff"]) > 0:
+            substat_1 = rune["sec_eff"][0][0]
+            substat_1_value = rune["sec_eff"][0][1]
+            substat_1_enchant = rune["sec_eff"][0][2]
+            substat_1_grind = rune["sec_eff"][0][3]
+        else:
+            substat_1 = None
+            substat_1_value = 0
+            substat_1_enchant = 0
+            substat_1_grind = 0
+        if len(rune["sec_eff"]) > 1:
+            substat_2 = rune["sec_eff"][1][0]
+            substat_2_value = rune["sec_eff"][1][1]
+            substat_2_enchant = rune["sec_eff"][1][2]
+            substat_2_grind = rune["sec_eff"][1][3]
+        else:
+            substat_2 = None
+            substat_2_value = 0
+            substat_2_enchant = 0
+            substat_2_grind = 0
+        if len(rune["sec_eff"]) > 2:
+            substat_3 = rune["sec_eff"][2][0]
+            substat_3_value = rune["sec_eff"][2][1]
+            substat_3_enchant = rune["sec_eff"][2][2]
+            substat_3_grind = rune["sec_eff"][2][3]
+        else:
+            substat_3 = None
+            substat_3_value = 0
+            substat_3_enchant = 0
+            substat_3_grind = 0
+        if len(rune["sec_eff"]) > 3:
+            substat_4 = rune["sec_eff"][3][0]
+            substat_4_value = rune["sec_eff"][3][1]
+            substat_4_enchant = rune["sec_eff"][3][2]
+            substat_4_grind = rune["sec_eff"][3][3]
+        else:
+            substat_4 = None
+            substat_4_value = 0
+            substat_4_enchant = 0
+            substat_4_grind = 0
+
+        # Calculate efficiences
+        efficiency = 0
+        substats = []
+        efficiency += float(MAIN_STAT_VALUES[main_stat][stars][15]) / float(MAIN_STAT_VALUES[main_stat][6][15])
+        if innate_stat is not None:
+            efficiency += innate_stat_value / float(SUBSTAT_INCREMENTS[innate_stat][6] * 5)
+        if substat_1 is not None:
+            substats.append(substat_1)
+            efficiency += substat_1_value / float(SUBSTAT_INCREMENTS[substat_1][6] * 5)
+        if substat_2 is not None:
+            substats.append(substat_2)
+            efficiency += substat_2_value / float(SUBSTAT_INCREMENTS[substat_2][6] * 5)
+        if substat_3 is not None:
+            substats.append(substat_3)
+            efficiency += substat_3_value / float(SUBSTAT_INCREMENTS[substat_3][6] * 5)
+        if substat_4 is not None:
+            substats.append(substat_4)
+            efficiency += substat_4_value / float(SUBSTAT_INCREMENTS[substat_4][6] * 5)
+        efficiency = efficiency / 2.8 * 100
+        max_efficiency = get_max_efficiency(substats, efficiency, level, stars, original_quality)
+        if efficiency > 100:
+            efficiency = 100;
+        if max_efficiency > 100:
+            max_efficiency = 100;
+        insert(db, "rune", (uid, id, assigned_to, runeset, slot, stars, ancient, level, quality, original_quality, value, efficiency, max_efficiency, main_stat, main_stat_value, innate_stat, innate_stat_value, substat_1, substat_1_value, substat_1_enchant, substat_1_grind, substat_2, substat_2_value, substat_2_enchant, substat_2_grind, substat_3, substat_3_value, substat_3_enchant, substat_3_grind, substat_4, substat_4_value, substat_4_enchant, substat_4_grind))
+    # Equipped runes
+    for mon in data["unit_list"]:
+        for rune in mon["runes"]:
+            id = rune["rune_id"]
+            assigned_to = rune["occupied_id"]
+            if assigned_to == 0:
+                assigned_to = None
+            runeset = rune["set_id"]
+            slot = rune["slot_no"]
+            stars = rune["class"]
+            ancient = 0
+            level = rune["upgrade_curr"]
+            original_quality = rune["extra"]
+            value = rune["sell_value"]
+            if stars > 10:
+                ancient = 1
+                stars = stars - 10
+                original_quality = original_quality - 10
+            if level >= 12:
+                quality = 5
+            elif level >= 9:
+                quality = 5
+            elif level >= 6:
+                quality = 3
+            elif level >= 3:
+                quality = 2
+            else:
+                quality = 1
+            if original_quality > quality:
+                quality = original_quality
+            efficiency = 0
+            max_efficiency = 0
+            main_stat = rune["pri_eff"][0]
+            main_stat_value = rune["pri_eff"][1]
+            if rune["prefix_eff"][0] > 0:
+                innate_stat = rune["prefix_eff"][0]
+                innate_stat_value = rune["prefix_eff"][1]
+            else:
+                innate_stat = None
+                innate_stat_value = 0
+            if len(rune["sec_eff"]) > 0:
+                substat_1 = rune["sec_eff"][0][0]
+                substat_1_value = rune["sec_eff"][0][1]
+                substat_1_enchant = rune["sec_eff"][0][2]
+                substat_1_grind = rune["sec_eff"][0][3]
+            else:
+                substat_1 = None
+                substat_1_value = 0
+                substat_1_enchant = 0
+                substat_1_grind = 0
+            if len(rune["sec_eff"]) > 1:
+                substat_2 = rune["sec_eff"][1][0]
+                substat_2_value = rune["sec_eff"][1][1]
+                substat_2_enchant = rune["sec_eff"][1][2]
+                substat_2_grind = rune["sec_eff"][1][3]
+            else:
+                substat_2 = None
+                substat_2_value = 0
+                substat_2_enchant = 0
+                substat_2_grind = 0
+            if len(rune["sec_eff"]) > 2:
+                substat_3 = rune["sec_eff"][2][0]
+                substat_3_value = rune["sec_eff"][2][1]
+                substat_3_enchant = rune["sec_eff"][2][2]
+                substat_3_grind = rune["sec_eff"][2][3]
+            else:
+                substat_3 = None
+                substat_3_value = 0
+                substat_3_enchant = 0
+                substat_3_grind = 0
+            if len(rune["sec_eff"]) > 3:
+                substat_4 = rune["sec_eff"][3][0]
+                substat_4_value = rune["sec_eff"][3][1]
+                substat_4_enchant = rune["sec_eff"][3][2]
+                substat_4_grind = rune["sec_eff"][3][3]
+            else:
+                substat_4 = None
+                substat_4_value = 0
+                substat_4_enchant = 0
+                substat_4_grind = 0
+
+            # Calculate efficiences
+            efficiency = 0
+            substats = []
+            efficiency += float(MAIN_STAT_VALUES[main_stat][stars][15]) / float(MAIN_STAT_VALUES[main_stat][6][15])
+            if innate_stat is not None:
+                efficiency += innate_stat_value / float(SUBSTAT_INCREMENTS[innate_stat][6] * 5)
+            if substat_1 is not None:
+                substats.append(substat_1)
+                efficiency += substat_1_value / float(SUBSTAT_INCREMENTS[substat_1][6] * 5)
+            if substat_2 is not None:
+                substats.append(substat_2)
+                efficiency += substat_2_value / float(SUBSTAT_INCREMENTS[substat_2][6] * 5)
+            if substat_3 is not None:
+                substats.append(substat_3)
+                efficiency += substat_3_value / float(SUBSTAT_INCREMENTS[substat_3][6] * 5)
+            if substat_4 is not None:
+                substats.append(substat_4)
+                efficiency += substat_4_value / float(SUBSTAT_INCREMENTS[substat_4][6] * 5)
+            efficiency = efficiency / 2.8 * 100
+            max_efficiency = get_max_efficiency(substats, efficiency, level, stars, original_quality)
+            if efficiency > 100:
+                efficiency = 100;
+            if max_efficiency > 100:
+                max_efficiency = 100;
+            insert(db, "rune", (uid, id, assigned_to, runeset, slot, stars, ancient, level, quality, original_quality, value, efficiency, max_efficiency, main_stat, main_stat_value, innate_stat, innate_stat_value, substat_1, substat_1_value, substat_1_enchant, substat_1_grind, substat_2, substat_2_value, substat_2_enchant, substat_2_grind, substat_3, substat_3_value, substat_3_enchant, substat_3_grind, substat_4, substat_4_value, substat_4_enchant, substat_4_grind))
+    db.commit()
+
+"""
+calculates the max efficiency of a rune.
+
+:param substats: Array of substat types, dont include empty ones.
+:param efficiency: Current rune efficiency.
+:param level: Current rune level.
+:param stars: Rune stars.
+:param quality: Rune original quality.
+:return Max. efficiency.
+"""
+def get_max_efficiency(substats, efficiency, level, stars, quality):
+    STAT_HP = 1
+    STAT_HP_PCT = 2
+    STAT_ATK = 3
+    STAT_ATK_PCT = 4
+    STAT_DEF = 5
+    STAT_DEF_PCT = 6
+    STAT_SPD = 8
+    STAT_CRIT_RATE_PCT = 9
+    STAT_CRIT_DMG_PCT = 10
+    STAT_RESIST_PCT = 11
+    STAT_ACCURACY_PCT = 12
+    SUBSTAT_INCREMENTS = {
+        # [stat][stars]: value
+        STAT_HP: {
+            1: 60,
+            2: 105,
+            3: 165,
+            4: 225,
+            5: 300,
+            6: 375,
+        },
+        STAT_HP_PCT: {
+            1: 2,
+            2: 3,
+            3: 5,
+            4: 6,
+            5: 7,
+            6: 8,
+        },
+        STAT_ATK: {
+            1: 4,
+            2: 5,
+            3: 8,
+            4: 10,
+            5: 15,
+            6: 20,
+        },
+        STAT_ATK_PCT: {
+            1: 2,
+            2: 3,
+            3: 5,
+            4: 6,
+            5: 7,
+            6: 8,
+        },
+        STAT_DEF: {
+            1: 4,
+            2: 5,
+            3: 8,
+            4: 10,
+            5: 15,
+            6: 20,
+        },
+        STAT_DEF_PCT: {
+            1: 2,
+            2: 3,
+            3: 5,
+            4: 6,
+            5: 7,
+            6: 8,
+        },
+        STAT_SPD: {
+            1: 1,
+            2: 2,
+            3: 3,
+            4: 4,
+            5: 5,
+            6: 6,
+        },
+        STAT_CRIT_RATE_PCT: {
+            1: 1,
+            2: 2,
+            3: 3,
+            4: 4,
+            5: 5,
+            6: 6,
+        },
+        STAT_CRIT_DMG_PCT: {
+            1: 2,
+            2: 3,
+            3: 4,
+            4: 5,
+            5: 6,
+            6: 7,
+        },
+        STAT_RESIST_PCT: {
+            1: 2,
+            2: 3,
+            3: 5,
+            4: 6,
+            5: 7,
+            6: 8,
+        },
+        STAT_ACCURACY_PCT: {
+            1: 2,
+            2: 3,
+            3: 5,
+            4: 6,
+            5: 7,
+            6: 8,
+        },
+    }
+    UPGRADE_VALUES = {
+        rune_type: {
+            stars: value/level_data[6]
+            for stars, value in level_data.items()
+        }
+        for rune_type, level_data in SUBSTAT_INCREMENTS.items()
+    }
+    #substat_upgrades_remaining = (4 - math.floor((min(level, 12) / 3))) - (3 - quality)
+    substat_upgrades_remaining = (4 - math.floor((min(level, 12) / 3))) - max((3 - quality), 0)
+    new_stats = max(min(4 - len(substats), substat_upgrades_remaining), 0)
+    old_stats = substat_upgrades_remaining - new_stats
+
+    if old_stats > 0:
+        # we can repeatedly upgrade the most value of the existing stats
+        best_stat = max(
+            0,  # ensure max() doesn't error if we only have one stat
+            *[UPGRADE_VALUES[stat][stars] for stat in substats]
+        )
+        efficiency += best_stat * old_stats * 0.2 / 2.8 * 100
+    if new_stats:
+        # add the top N stats
+        available_upgrades = sorted(
+            [
+                upgrade_value[stars]
+                for stat, upgrade_value in UPGRADE_VALUES.items()
+                if stat not in substats
+            ],
+            reverse=True
+        )
+        efficiency += sum(available_upgrades[:new_stats]) * 0.2 / 2.8 * 100
+
+    return efficiency
+
+"""
+Parses rune craft item data (table rune_craft).
+
+:param db: Sqlite database connection.
+:param data: Data in json format.
+"""
+def parseRuneCraft(db, data):
+    print("Parsing rune craft itmes...")
+    uid = data["wizard_info"]["wizard_id"]
+    for item in data["rune_craft_item_list"]:
+        id = item["craft_item_id"]
+        type = item["craft_type"]
+        value = item["sell_value"]
+        info = str(item["craft_type_id"])
+        """
+        info : 5 or 6 digit number: RRSSQ
+          RR: Rune
+          SS: Stat
+          Q:  Quality
+        """
+        quality = int(info[-1:])
+        stat = int(info[-4:-2])
+        rune = int(info[:-4])
+        insert(db, "rune_craft", (uid, id, type, quality, rune, stat, value))
+
+"""
+Parses guild data (tables guild, guild_member).
+
+:param db: Sqlite database connection.
+:param data: Data in json format.
+"""
+def parseGuild(db, data):
+    print("Parsing guild...")
+    cursor = db.cursor()
+    if (data["guild"]["guild_info"] == None):
+        # No guild
+        return
+    if len(data["guild"]["guild_info"]) < 1:
+        return
+    guild = data["guild"]["guild_info"]
+    id = guild["guild_id"]
+    name = guild["name"]
+    level = guild["level"]
+    experience = guild["experience"]
+    recruiting = guild["recruit_status"]
+    members = guild["member_now"]
+    leader = guild["master_wizard_id"]
+    comment = guild["comment"]
+    notice = guild["notice"]
+    # Delete selected guild before inserting
+    cursor.execute("DELETE FROM guild WHERE id = ?", [id])
+    cursor.execute("DELETE FROM guild_member WHERE guild = ?", [id])
+    db.commit()
+    insert(db, "guild", (id, name, level, experience, recruiting, members, leader, comment, notice))
+    # Single quates ahead, dirty fix
+    for k, member in data["guild"]["guild_members"].items():
+        m = json.loads(str(member).replace("'", '"'))
+        id = m["wizard_id"]
+        guild = member["guild_id"]
+        grade = member["grade"]
+        name = member["wizard_name"]
+        level = member["wizard_level"]
+        rating = member["rating_id"]
+        arena_score = member["arena_score"]
+        joined = member["join_timestamp"]
+        last_login = member["last_login_timestamp"]
+        in_war = 0 # Checked later
+        has_defense = 0 # Checked later
+        insert(db, "guild_member", (id, guild, grade, name, level, rating, arena_score, joined, last_login, in_war, has_defense))
+    for war in data["guildwar_member_list"]:
+        cursor.execute("UPDATE guild_member SET in_war = 1 WHERE guild = ? AND id = ?;", (war["guild_id"], war["wizard_id"]))
+    for defense in data["guild_member_defense_list"]:
+        if len(defense["unit_list"]) > 0:
+            cursor.execute("UPDATE guild_member SET has_defense = 1 WHERE id = ?", [defense["wizard_id"]])
+    db.commit()
+    cursor.close()
+
+"""
+Deletes teams whose members no longer exists.
+
+:param db: Sqlite database connection.
+"""
+def reconfigureTeams(db):
+    print("Fixing teams...")
+    uid = data["wizard_info"]["wizard_id"]
+    cursor = db.cursor()
+    cursor.execute("DELETE FROM team WHERE id IN (SELECT DISTINCT team FROM team_unit WHERE unit NOT IN (SELECT DISTINCT id FROM unit WHERE uid = ?)) AND uid = ?",  (uid, uid))
+    cursor.execute("DELETE FROM team_unit WHERE unit NOT IN (SELECT DISTINCT id FROM unit) OR team NOT IN (SELECT DISTINCT id FROM team);")
+    db.commit()
+    cursor.close()
+
+"""
+Begin script
+"""
+try:
+    data = readData()
+except Exception as e:
+    print("Error reading data: " + str(e))
+    sys.exit(400)
+try:
+    key = readKey()
+except Exception as e:
+    print("Error fetching API key: " + str(e))
+    sys.exit(400)
+try:
+    db = openDatabase()
+except Exception as e:
+    print("Error opening database: " + str(e))
+    sys.exit(500)
+if verifyKey(db, data, key) == False:
+    print("Invalid API KEY...")
+    sys.exit(401)
+else:
+    try:
+        clearData(db, data)
+    except Exception as e:
+        print("Error clearing data: " + str(e))
+        sys.exit(500)
+    try:
+        parsePlayer(db, data)
+    except Exception as e:
+        print("Error parsing player data: " + str(e))
+        sys.exit(400)
+    try:
+        parseScenarios(db, data)
+    except Exception as e:
+        print("Error parsing scenario data: " + str(e))
+        sys.exit(400)
+    try:
+        parseDefense(db, data)
+    except Exception as e:
+        print("Error parsing defense data: " + str(e))
+        sys.exit(400)
+    try:
+        parseBuildings(db, data)
+    except Exception as e:
+        print("Error parsing building data: " + str(e))
+        sys.exit(400)
+    try:
+        parseDecorations(db, data)
+    except Exception as e:
+        print("Error parsing decoration data: " + str(e))
+        sys.exit(400)
+    # TODO homunculus_skill_list
+    try:
+        parseUnits(db, data)
+    except Exception as e:
+        print("Error parsing unit data: " + str(e))
+        sys.exit(400)
+    try:
+        parseSummonSpecial(db, data)
+    except Exception as e:
+        print("Error parsing special summon data: " + str(e))
+        sys.exit(400)
+    try:
+        parseInventory(db, data)
+    except Exception as e:
+        print("Error parsing inventory data: " + str(e))
+        sys.exit(400)
+    try:
+        parseRunes(db, data)
+    except Exception as e:
+        print("Error parsing runes data: " + str(e))
+        sys.exit(400)
+    try:
+        parseRuneCraft(db, data)
+    except Exception as e:
+        print("Error parsing rune craft data: " + str(e))
+        sys.exit(400)
+    try:
+        parseGuild(db, data)
+    except Exception as e:
+        print("Error parsing guild data: " + str(e))
+        sys.exit(400)
+    try:
+        reconfigureTeams(db)
+    except Exception as e:
+        print("Error reconfiguring teams: " + str(e))
+        sys.exit(400)
+    try:
+        closeDatabase(db)
+    except Exception as e:
+        print("Error closing database: " + str(e))
+        # No error
+sys.exit(200);
+

+ 246 - 0
application/API/v2/bin/upload_run_dimension.py

@@ -0,0 +1,246 @@
+#!/usr/bin/python3
+
+import sqlite3
+import json
+import sys
+import os
+
+import MAPPING
+
+"""
+Reads the API KEY, that must be passed as first command line argument.
+
+:returns: Recovered API KEY.
+:raises Exception: Th KEY couldn't be red.
+"""
+def readKey():
+    try:
+        key = sys.argv[1]
+        return key
+    except Exception as e:
+        print("Error parsing API KEY: " + str(e))
+        raise
+
+"""
+Reads the JSON data, that must be passed as second command line argument.
+
+:param: index 2 for start data, 3 for result data
+:returns: Recovered data, in JSON format.
+:raises Exception: The data couldn't be red or converted to JSON.
+"""
+def readData(index):
+    try:
+        data = json.loads(sys.argv[index])
+        return data
+    except Exception as e:
+        print("Error parsing data: " + str(e))
+        raise
+
+"""
+Verifies that the API key matches the player data and that it exists in th DB.
+
+:param db: Connection to the database.
+:returns: Connection to the database.
+:param data: Data in json format.
+:param key: API KEY.
+:returns: True if key and player match, False otherwise.
+:raises IntegrityError: The queryes couldn't bre executed.
+"""
+def verifyKey(db, data, key):
+    print('Verifying KEY...')
+    status = False
+    try:
+        uid = data["wizard_info"]["wizard_id"]
+        cursor = db.cursor()
+        cursor.execute('SELECT count(uid) AS c FROM player WHERE uid = ? AND api_key = ?;', (uid, key))
+        if cursor.fetchone()[0] == 1:
+            status = True
+        cursor.close()
+    except sqlite3.IntegrityError as e:
+        print("Error executing statement: " + str(e))
+        raise
+    return status
+
+"""
+Opens the database file and deletes from the user tables
+
+:param name: The path to the sqlite database.
+:returns: Connection to the database.
+:raises IntegrityError: The queryes couldn't bre executed.
+:raises IOError: The sqlite file couldn't be created.
+"""
+def openDatabase():
+    kdb = os.path.dirname(os.path.realpath(sys.argv[0])) + '/../../../sw.sqlite'
+    udb = os.path.dirname(os.path.realpath(sys.argv[0])) + '/../../../../data/sw.sqlite'
+    print('Configuring database...')
+    try:
+        db = sqlite3.connect(kdb)
+        cursor = db.cursor()
+        cursor.execute('attach "' + udb + '" as data;')
+        cursor.close()
+        return db
+    except sqlite3.IntegrityError as e:
+        print("Error executing statement: " + str(e))
+        raise
+    except IOError as e:
+        print("I/O Error creating database " + name + ": " + str(e))
+        raise
+
+"""
+Inserts a row into the database.
+
+:param db: Connection to the database.
+:param table: Name of the table to insert into.
+:param values: List of values to insert.
+:raises IntegrityError: The insert query was unsuccesfull.
+"""
+def insert(db, table, values):
+    cursor = db.cursor()
+    placeholders = ''
+    for x in range(0, len(values)):
+        placeholders = placeholders + '?, '
+    placeholders = placeholders[:len(placeholders) - 2]
+    query = 'INSERT INTO ' + table + ' VALUES (' + placeholders + ')'
+    try:
+        cursor.execute(query, values)
+    except sqlite3.IntegrityError as e:
+        print('Error Inserting into ' + table + ' with values (' + str(values) + '): ' + str(e))
+        raise
+    cursor.close;
+
+"""
+Parses run data (tables run, run_party, run_drop_rune, run_drop_runecraft,
+run_drop_item, run_drop_unit, run_drop_sd, run_drop_unit_pieces,
+run_drop_shapeshifting).
+
+:param db: Sqlite database connection.
+:param data_start: JSON data of the run start.
+:param data_result: JSON data of the run result.
+"""
+def parseRun(db, data_request, data_response):
+    print("Parsing run...")
+    cursor = db.cursor()
+
+    # Read basic data
+    uid = data_request["wizard_id"]
+    dtime = data_response["tvaluelocal"]
+
+    # Check if run has already been inserted.
+    cursor.execute("SELECT count(id) AS c FROM run WHERE uid = ? AND dtime = ?;", (uid, dtime))
+    if (cursor.fetchone()[0] > 0):
+        print("    Run already in database. Stopping....")
+        return 409;
+
+    # We are inserting, get aditional info
+    area_type = 5 # Dimension Hole Dungeon
+    area = data_request["dungeon_id"]
+    stage = data_request["difficulty"] # Stage is difficulty
+    difficulty = None
+    win = data_response["win_lose"]
+    time = data_response["clear_time"]["current_time"]
+    if ("mana" in data_response["reward"]):
+        mana = data_response["reward"]["mana"]
+    else:
+        mana = 0
+    if ("energy" in data_response["reward"]):
+        energy = mana = data_response["reward"]["energy"]
+    else:
+        energy = 0
+    if ("crystal" in data_response["reward"]):
+        crystal = mana = data_response["reward"]["crystal"]
+    else:
+        crystal = 0
+    helper = 0
+
+    # Get the new ID and insert
+    cursor.execute("SELECT max(id) + 1 AS id FROM run;")
+    id = cursor.fetchone()[0]
+    if id == None:
+        id = 1
+    insert(db, "run", (uid, id, dtime, area_type, area, stage, difficulty, win, time, mana, energy, crystal, helper))
+
+    # Parse various types of reward
+    crate = data_response["reward"]["crate"]
+
+    if "item" in crate:
+        for item in crate["item"]:
+            insert(db, "run_drop_item", (id, item["item_id"], item["item_quantity"]))
+    if "craft_stuff" in crate:
+        for item in crate["craft_stuff"]:
+            insert(db, ".run_drop_rune_craft", (id, item["item_id"], item["item_quantity"]))
+
+    # Parse rune reward
+    if "rune" in crate:
+        rune = crate["rune"]
+        rune_id = rune["rune_id"]
+        rune_type = rune["set_id"]
+        slot = rune["slot_no"]
+        stars = rune["class"]
+        ancient = 0
+        quality = rune["rank"]
+        value = rune["sell_value"]
+        efficiency, max_efficiency = MAPPING.calculate_efficiency(rune)
+        main_stat = rune["pri_eff"][0]
+        main_stat_value = rune["pri_eff"][1]
+        if "prefix_eff" in rune:
+            innate_stat = rune["prefix_eff"][0]
+            innate_stat_value = rune["prefix_eff"][1]
+        else:
+            innate_stat = 0
+            innate_stat_value = 0
+        substat_1 = 0
+        substat_1_value = 0
+        substat_2 = 0
+        substat_2_value = 0
+        substat_3 = 0
+        substat_3_value = 0
+        substat_4 = 0
+        substat_4_value = 0
+        if "0" in rune["sec_eff"]:
+            substat_1 = rune["sec_eff"]["0"]["0"]
+            substat_1_value = rune["sec_eff"]["0"]["1"]
+        if "1" in rune["sec_eff"]:
+            substat_1 = rune["sec_eff"]["1"]["0"]
+            substat_1_value = rune["sec_eff"]["1"]["1"]
+        if "2" in rune["sec_eff"]:
+            substat_1 = rune["sec_eff"]["2"]["0"]
+            substat_1_value = rune["sec_eff"]["2"]["1"]
+        if "3" in rune["sec_eff"]:
+            substat_1 = rune["sec_eff"]["3"]["0"]
+            substat_1_value = rune["sec_eff"]["3"]["1"]
+        insert(db, "run_drop_rune", (id, rune_id, rune_type, slot, stars, ancient, quality, value, efficiency, max_efficiency, main_stat, main_stat_value, innate_stat, innate_stat_value, substat_1, substat_1_value, substat_2, substat_2_value, substat_3, substat_3_value, substat_4, substat_4_value))
+
+    # Parse units
+    for unit in data_request["unit_id_list"]:
+        unit_id = unit["unit_id"]
+        unit_master_id = None
+        for k_unit in data_response["unit_list"]:
+            if k_unit["unit_id"] == unit["unit_id"]:
+                unit_master_id = k_unit["unit_master_id"];
+                break;
+        #unit_master_id = data_response["unit_list"][attribute]["unit_master_id"]
+        leader = unit["is_leader"]
+        front = 0
+        insert(db, "run_party", (id, unit_master_id, unit_id, leader, front))
+    db.commit()
+
+
+"""
+Begin script
+"""
+data_request = readData(2)
+data_response = readData(3)
+key = readKey()
+db = openDatabase()
+if verifyKey(db, data_response, key) == False:
+    print("Invalid API KEY...")
+    sys.exit(401)
+else:
+    try:
+        parseRun(db, data_request, data_response)
+    except Exception as e:
+        print("Error parsing Dimension Hole Dungeon run: " + str(e))
+        sys.exit(400)
+sys.exit(201)
+
+

+ 252 - 0
application/API/v2/bin/upload_run_dungeon.py

@@ -0,0 +1,252 @@
+#!/usr/bin/python3
+
+import sqlite3
+import json
+import sys
+import os
+
+import MAPPING
+
+"""
+Reads the API KEY, that must be passed as first command line argument.
+
+:returns: Recovered API KEY.
+:raises Exception: Th KEY couldn't be red.
+"""
+def readKey():
+    try:
+        key = sys.argv[1]
+        return key
+    except Exception as e:
+        print("Error parsing API KEY: " + str(e))
+        raise
+
+"""
+Reads the JSON data, that must be passed as second command line argument.
+
+:param: index 2 for start data, 3 for result data
+:returns: Recovered data, in JSON format.
+:raises Exception: The data couldn't be red or converted to JSON.
+"""
+def readData(index):
+    try:
+        data = json.loads(sys.argv[index])
+        return data
+    except Exception as e:
+        print("Error parsing data: " + str(e))
+        raise
+
+"""
+Verifies that the API key matches the player data and that it exists in th DB.
+
+:param db: Connection to the database.
+:returns: Connection to the database.
+:param data: Data in json format.
+:param key: API KEY.
+:returns: True if key and player match, False otherwise.
+:raises IntegrityError: The queryes couldn't bre executed.
+"""
+def verifyKey(db, data, key):
+    print('Verifying KEY...')
+    status = False
+    try:
+        uid = data["wizard_info"]["wizard_id"]
+        cursor = db.cursor()
+        cursor.execute('SELECT count(uid) AS c FROM player WHERE uid = ? AND api_key = ?;', (uid, key))
+        if cursor.fetchone()[0] == 1:
+            status = True
+        cursor.close()
+    except sqlite3.IntegrityError as e:
+        print("Error executing statement: " + str(e))
+        raise
+    return status
+
+"""
+Opens the database file and deletes from the user tables
+
+:param name: The path to the sqlite database.
+:returns: Connection to the database.
+:raises IntegrityError: The queryes couldn't bre executed.
+:raises IOError: The sqlite file couldn't be created.
+"""
+def openDatabase():
+    kdb = os.path.dirname(os.path.realpath(sys.argv[0])) + '/../../../sw.sqlite'
+    udb = os.path.dirname(os.path.realpath(sys.argv[0])) + '/../../../../data/sw.sqlite'
+    print('Configuring database...')
+    try:
+        db = sqlite3.connect(kdb)
+        cursor = db.cursor()
+        cursor.execute('attach "' + udb + '" as data;')
+        cursor.close()
+        return db
+    except sqlite3.IntegrityError as e:
+        print("Error executing statement: " + str(e))
+        raise
+    except IOError as e:
+        print("I/O Error creating database " + name + ": " + str(e))
+        raise
+
+"""
+Inserts a row into the database.
+
+:param db: Connection to the database.
+:param table: Name of the table to insert into.
+:param values: List of values to insert.
+:raises IntegrityError: The insert query was unsuccesfull.
+"""
+def insert(db, table, values):
+    cursor = db.cursor()
+    placeholders = ''
+    for x in range(0, len(values)):
+        placeholders = placeholders + '?, '
+    placeholders = placeholders[:len(placeholders) - 2]
+    query = 'INSERT INTO ' + table + ' VALUES (' + placeholders + ')'
+    try:
+        cursor.execute(query, values)
+    except sqlite3.IntegrityError as e:
+        print('Error Inserting into ' + table + ' with values (' + str(values) + '): ' + str(e))
+        raise
+    cursor.close;
+
+"""
+Parses run data (tables run, run_party, run_drop_rune, run_drop_runecraft,
+run_drop_item, run_drop_unit, run_drop_sd, run_drop_unit_pieces,
+run_drop_shapeshifting).
+
+:param db: Sqlite database connection.
+:param data_start: JSON data of the run start.
+:param data_result: JSON data of the run result.
+"""
+def parseRun(db, data_request, data_response):
+    print("Parsing run...")
+    cursor = db.cursor()
+
+    # Read basic data
+    uid = data_request["wizard_id"]
+    dtime = data_response["tvaluelocal"]
+
+    # Check if run has already been inserted.
+    cursor.execute("SELECT count(id) AS c FROM run WHERE uid = ? AND dtime = ?;", (uid, dtime))
+    if (cursor.fetchone()[0] > 0):
+        print("    Run already in database. Stopping....")
+        return 409;
+
+    # We are inserting, get aditional info
+    area_type = 2 # Cairos Dungeon
+    area = data_request["dungeon_id"]
+    stage = data_request["stage_id"]
+    difficulty = None
+    win = data_response["win_lose"]
+    time = data_response["clear_time"]["current_time"]
+    if ("mana" in data_response["reward"]):
+        mana = data_response["reward"]["mana"]
+    else:
+        mana = 0
+    if ("energy" in data_response["reward"]):
+        energy = mana = data_response["reward"]["energy"]
+    else:
+        energy = 0
+    if ("crystal" in data_response["reward"]):
+        crystal = mana = data_response["reward"]["crystal"]
+    else:
+        crystal = 0
+    # TODO Read helper. Do a test with SWBD-debug
+    helper = 0
+
+    # GEt the new ID and insert
+    cursor.execute("SELECT max(id) + 1 AS id FROM run;")
+    id = cursor.fetchone()[0]
+    if id == None:
+        id = 1
+    insert(db, "run", (uid, id, dtime, area_type, area, stage, difficulty, win, time, mana, energy, crystal, helper))
+
+    # Parse various types of reward
+    crate = data_response["reward"]["crate"]
+    if "costume_point" in crate and crate["costume_point"] > 0:
+        insert(db, "run_drop_shapeshifting", (id, crate["costume_point"]))
+    if "instance_info" in data_response:
+        insert(db, "run_drop_sd", (id, data_response["instance_info"]))
+    if "unit_info" in crate:
+        insert(db, "run_drop_unit", (id, crate["unit_info"]["unit_master_id"]))
+    if "summon_pieces" in crate:
+        insert(db, "run_drop_unit_pieces", (id, crate["summon_pieces"]["item_master_id"], crate["summon_pieces"]["item_quantity"]))
+    if "item" in crate:
+        for item in crate["item"]:
+            insert(db, "run_drop_item", (id, item["item_id"], item["item_quantity"]))
+
+    # Parse rune reward
+    if "rune" in crate:
+        rune = crate["rune"]
+        rune_id = rune["rune_id"]
+        rune_type = rune["set_id"]
+        slot = rune["slot_no"]
+        stars = rune["class"]
+        ancient = 0
+        quality = rune["rank"]
+        value = rune["sell_value"]
+        efficiency, max_efficiency = MAPPING.calculate_efficiency(rune)
+        main_stat = rune["pri_eff"][0]
+        main_stat_value = rune["pri_eff"][1]
+        if "prefix_eff" in rune:
+            innate_stat = rune["prefix_eff"][0]
+            innate_stat_value = rune["prefix_eff"][1]
+        else:
+            innate_stat = 0
+            innate_stat_value = 0
+        substat_1 = 0
+        substat_1_value = 0
+        substat_2 = 0
+        substat_2_value = 0
+        substat_3 = 0
+        substat_3_value = 0
+        substat_4 = 0
+        substat_4_value = 0
+        if "0" in rune["sec_eff"]:
+            substat_1 = rune["sec_eff"]["0"]["0"]
+            substat_1_value = rune["sec_eff"]["0"]["1"]
+        if "1" in rune["sec_eff"]:
+            substat_1 = rune["sec_eff"]["1"]["0"]
+            substat_1_value = rune["sec_eff"]["1"]["1"]
+        if "2" in rune["sec_eff"]:
+            substat_1 = rune["sec_eff"]["2"]["0"]
+            substat_1_value = rune["sec_eff"]["2"]["1"]
+        if "3" in rune["sec_eff"]:
+            substat_1 = rune["sec_eff"]["3"]["0"]
+            substat_1_value = rune["sec_eff"]["3"]["1"]
+        insert(db, "run_drop_rune", (id, rune_id, rune_type, slot, stars, ancient, quality, value, efficiency, max_efficiency, main_stat, main_stat_value, innate_stat, innate_stat_value, substat_1, substat_1_value, substat_2, substat_2_value, substat_3, substat_3_value, substat_4, substat_4_value))
+
+    # Parse units
+    #for attribute, value in data_request["unit_id_list"]:
+    for unit in data_request["unit_id_list"]:
+        unit_id = unit["unit_id"]
+        unit_master_id = None
+        for k_unit in data_response["unit_list"]:
+            if k_unit["unit_id"] == unit["unit_id"]:
+                unit_master_id = k_unit["unit_master_id"];
+                break;
+        #unit_master_id = data_response["unit_list"][attribute]["unit_master_id"]
+        leader = unit["is_leader"]
+        front = 0
+        insert(db, "run_party", (id, unit_master_id, unit_id, leader, front))
+    db.commit()
+
+
+"""
+Begin script
+"""
+data_request = readData(2)
+data_response = readData(3)
+key = readKey()
+db = openDatabase()
+if verifyKey(db, data_response, key) == False:
+    print("Invalid API KEY...")
+    sys.exit(401)
+else:
+    #try:
+        parseRun(db, data_request, data_response)
+    #except Exception as e:
+    #    print("Error parsing dungeon run: " + str(e))
+    #    sys.exit(400)
+sys.exit(201)
+
+

+ 207 - 0
application/API/v2/bin/upload_run_scenario.py

@@ -0,0 +1,207 @@
+#!/usr/bin/python3
+
+import sqlite3
+import json
+import sys
+import os
+
+
+"""
+Reads the API KEY, that must be passed as first command line argument.
+
+:returns: Recovered API KEY.
+:raises Exception: Th KEY couldn't be red.
+"""
+def readKey():
+    try:
+        #print(sys.argv[0])
+        key = sys.argv[1]
+        return key
+    except Exception as e:
+        print("Error parsing API KEY: " + str(e))
+        raise
+
+"""
+Reads the JSON data, that must be passed as second command line argument.
+
+:returns: Recovered data, in JSON format.
+:raises Exception: The data couldn't be red or converted to JSON.
+"""
+def readData():
+    try:
+        data = json.loads(sys.argv[2])
+        return data
+    except Exception as e:
+        print("Error parsing data: " + str(e))
+        raise
+
+"""
+Verifies that the API key matches the player data and that it exists in th DB.
+
+:param db: Connection to the database.
+:returns: Connection to the database.
+:param data: Data in json format.
+:param key: API KEY.
+:returns: True if key and player match, False otherwise.
+:raises IntegrityError: The queryes couldn't bre executed.
+"""
+def verifyKey(db, data, key):
+    print('Verifying KEY...')
+    status = False
+    try:
+        uid = data["wizard_info"]["wizard_id"]
+        cursor = db.cursor()
+        cursor.execute('SELECT count(uid) AS c FROM player WHERE uid = ? AND api_key = ?;', (uid, key))
+        if cursor.fetchone()[0] == 1:
+            status = True
+        cursor.close()
+    except sqlite3.IntegrityError as e:
+        print("Error executing statement: " + str(e))
+        raise
+    return status
+
+"""
+Opens the database file and deletes from the user tables
+
+:param name: The path to the sqlite database.
+:returns: Connection to the database.
+:raises IntegrityError: The queryes couldn't bre executed.
+:raises IOError: The sqlite file couldn't be created.
+"""
+def openDatabase():
+    kdb = os.path.dirname(os.path.realpath(sys.argv[0])) + '/../../../sw.sqlite'
+    udb = os.path.dirname(os.path.realpath(sys.argv[0])) + '/../../../../data/sw.sqlite'
+    print('Configuring database...')
+    try:
+        db = sqlite3.connect(kdb)
+        cursor = db.cursor()
+        cursor.execute('attach "' + udb + '" as data;')
+        cursor.close()
+        return db
+    except sqlite3.IntegrityError as e:
+        print("Error executing statement: " + str(e))
+        raise
+    except IOError as e:
+        print("I/O Error creating database " + name + ": " + str(e))
+        raise
+
+"""
+Inserts a row into the database.
+
+:param db: Connection to the database.
+:param table: Name of the table to insert into.
+:param values: List of values to insert.
+:raises IntegrityError: The insert query was unsuccesfull.
+"""
+def insert(db, table, values):
+    cursor = db.cursor()
+    placeholders = ''
+    for x in range(0, len(values)):
+        placeholders = placeholders + '?, '
+    placeholders = placeholders[:len(placeholders) - 2]
+    query = 'INSERT INTO ' + table + ' VALUES (' + placeholders + ')'
+    try:
+        cursor.execute(query, values)
+    except sqlite3.IntegrityError as e:
+        print('Error Inserting into ' + table + ' with values (' + str(values) + '): ' + str(e))
+        raise
+    cursor.close;
+
+"""
+Parses run data (tables run, run_party, run_drop_rune, run_drop_runecraft,
+run_drop_item, run_drop_unit, run_drop_sd, run_drop_unit_pieces,
+run_drop_shapeshifting).
+
+:param db: Sqlite database connection.
+:param data: JSON data.
+"""
+def parseRun(db, data):
+    print("Parsing run...")
+    cursor = db.cursor()
+    uid = data["wizard_info"]["wizard_id"]
+    dtime = data["tvaluelocal"]
+    # Check if run has already been inserted.
+    cursor.execute("SELECT count(id) AS c FROM run WHERE uid = ? AND dtime = ?;", (uid, dtime))
+    if (cursor.fetchone()[0] > 0):
+        print("    Run already in database. Stopping....")
+        return;
+    area_type = 1 #scenario 
+    area = data["scenario_info"]["region_id"]
+    stage = data["scenario_info"]["stage_no"]
+    difficulty = data["scenario_info"]["difficulty"]
+    win = data["win_lose"]
+    time = data["clear_time"]["current_time"]
+    if ("mana" in data["reward"]):
+        mana = data["reward"]["mana"]
+    else:
+        mana = 0
+    if ("energy" in data["reward"]):
+        energy = mana = data["reward"]["energy"]
+    else:
+        energy = 0
+    if ("crystal" in data["reward"]):
+        crystal = mana = data["reward"]["crystal"]
+    else:
+        crystal = 0
+    helper = data["helper"]
+    cursor.execute("SELECT max(id) + 1 AS id FROM run;")
+    id = cursor.fetchone()[0]
+    if id == None:
+        id = 1
+    insert(db, "run", (uid, id, dtime, area, stage, difficulty, win, time, mana, energy, crystal, helper))
+    if data["shapeshifting"] > 0:
+        insert(db, "run_drop_shapeshifting", (id, data["shapeshifting"]))
+    if data["sd"] > 0:
+        insert(db, "run_drop_sd", (id, data["sd"]))
+    if data["sd"] > 0:
+        insert(db, "run_drop_unit", (id, data["unit"]))
+    for rune in data["rune"]:
+        rune_id = rune["id"]
+        rune_type = rune["type"]
+        slot = rune["slot"]
+        stars = rune["stars"]
+        ancient = rune["ancient"]
+        quality = rune["quality"]
+        value = rune["value"]
+        efficiency = rune["efficiency"]
+        main_stat = rune["main_stat"]
+        main_stat_value = rune["main_stat_value"]
+        innate_stat = rune["innate_stat"]
+        innate_stat_value = rune["innate_stat_value"]
+        substat_1 = rune["substat_1"]
+        substat_1_value = rune["substat_1_value"]
+        substat_2 = rune["substat_2"]
+        substat_2_value = rune["substat_2_value"]
+        substat_3 = rune["substat_3"]
+        substat_3_value = rune["substat_3_value"]
+        substat_4 = rune["substat_4"]
+        substat_4_value = rune["substat_4_value"]
+        insert(db, "run_drop_rune", (id, rune_id, rune_type, slot, stars, ancient, quality, value, efficiency, main_stat, main_stat_value, innate_stat, innate_stat_value, substat_1, substat_1_value, substat_2, substat_2_value, substat_3, substat_3_value, substat_4, substat_4_value))
+    for item in data["item"]:
+        item_id = item["id"]
+        quantity = item["quantity"]
+        insert(db, "run_drop_item", (id, item_id, quantity))
+    for pieces in data["unit_pieces"]:
+        pieces_id = pieces["id"]
+        quantity = pieces["quantity"]
+        insert(db, "run_drop_unit_pieces", (id, pieces_id, quantity))
+    for party in data["party"]:
+        unit_id = party["unit_id"]
+        unit_master_id = party["unit_master_id"]
+        insert(db, "run_party", (id, unit_master_id, unit_id))
+    db.commit()
+
+"""
+Begin script
+"""
+data = readData()
+key = readKey()
+db = openDatabase()
+if verifyKey(db, data, key) == False:
+    print("Invalid API KEY...")
+    sys.exit(-1)
+else:
+    parseRun(db, data)
+sys.exit(0)
+
+

+ 226 - 0
application/API/v2/bin/upload_run_toa.py

@@ -0,0 +1,226 @@
+#!/usr/bin/python3
+
+import sqlite3
+import json
+import sys
+import os
+import math
+
+"""
+Reads the API KEY, that must be passed as first command line argument.
+
+:returns: Recovered API KEY.
+:raises Exception: Th KEY couldn't be red.
+"""
+def readKey():
+    try:
+        key = sys.argv[1]
+        return key
+    except Exception as e:
+        print("Error parsing API KEY: " + str(e))
+        raise
+
+"""
+Reads the JSON data, that must be passed as second command line argument.
+
+:param: index 2 for start data, 3 for result data
+:returns: Recovered data, in JSON format.
+:raises Exception: The data couldn't be red or converted to JSON.
+"""
+def readData(index):
+    try:
+        data = json.loads(sys.argv[index])
+        return data
+    except Exception as e:
+        print("Error parsing data: " + str(e))
+        raise
+
+"""
+Verifies that the API key matches the player data and that it exists in th DB.
+
+:param db: Connection to the database.
+:returns: Connection to the database.
+:param data: Data in json format.
+:param key: API KEY.
+:returns: True if key and player match, False otherwise.
+:raises IntegrityError: The queryes couldn't bre executed.
+"""
+def verifyKey(db, data, key):
+    print('Verifying KEY...')
+    status = False
+    try:
+        uid = data["wizard_info"]["wizard_id"]
+        cursor = db.cursor()
+        cursor.execute('SELECT count(uid) AS c FROM player WHERE uid = ? AND api_key = ?;', (uid, key))
+        if cursor.fetchone()[0] == 1:
+            status = True
+        cursor.close()
+    except sqlite3.IntegrityError as e:
+        print("Error executing statement: " + str(e))
+        raise
+    return status
+
+"""
+Opens the database file and deletes from the user tables
+
+:param name: The path to the sqlite database.
+:returns: Connection to the database.
+:raises IntegrityError: The queryes couldn't bre executed.
+:raises IOError: The sqlite file couldn't be created.
+"""
+def openDatabase():
+    kdb = os.path.dirname(os.path.realpath(sys.argv[0])) + '/../../../sw.sqlite'
+    udb = os.path.dirname(os.path.realpath(sys.argv[0])) + '/../../../../data/sw.sqlite'
+    print('Configuring database...')
+    try:
+        db = sqlite3.connect(kdb)
+        cursor = db.cursor()
+        cursor.execute('attach "' + udb + '" as data;')
+        cursor.close()
+        return db
+    except sqlite3.IntegrityError as e:
+        print("Error executing statement: " + str(e))
+        raise
+    except IOError as e:
+        print("I/O Error opening database " + name + ": " + str(e))
+        raise
+
+"""
+Inserts a row into the database.
+
+:param db: Connection to the database.
+:param table: Name of the table to insert into.
+:param values: List of values to insert.
+:raises IntegrityError: The insert query was unsuccesfull.
+"""
+def insert(db, table, values):
+    cursor = db.cursor()
+    placeholders = ''
+    for x in range(0, len(values)):
+        placeholders = placeholders + '?, '
+    placeholders = placeholders[:len(placeholders) - 2]
+    query = 'INSERT INTO ' + table + ' VALUES (' + placeholders + ')'
+    try:
+        cursor.execute(query, values)
+    except sqlite3.IntegrityError as e:
+        print('Error Inserting into ' + table + ' with values (' + str(values) + '): ' + str(e))
+        raise
+    cursor.close;
+
+"""
+Parses run data (tables run, run_party, run_drop_rune, run_drop_runecraft,
+run_drop_item, run_drop_unit, run_drop_sd, run_drop_unit_pieces,
+run_drop_shapeshifting).
+
+:param db: Sqlite database connection.
+:param data_start: JSON data of the run start.
+:param data_result: JSON data of the run result.
+"""
+def parseRun(db, data_request, data_response):
+    print("Parsing run...")
+    cursor = db.cursor()
+
+    # Read basic data
+    uid = data_request["wizard_id"]
+    dtime = data_response["tvaluelocal"]
+
+    # Check if run has already been inserted.
+    cursor.execute("SELECT count(id) AS c FROM run WHERE uid = ? AND dtime = ?;", (uid, dtime))
+    if (cursor.fetchone()[0] > 0):
+        print("    Run already in database. Stopping....")
+        return 409;
+
+    # We are inserting, get aditional info
+    area_type = 10 # TOA
+    area = 10 # TOA
+    stage = data_request["floor_id"]
+    difficulty = data_request["difficulty"]
+    win = data_response["win_lose"]
+    time = data_request["clear_time"]
+    helper = 0
+
+    # Get the new ID and insert
+    cursor.execute("SELECT max(id) + 1 AS id FROM run;")
+    id = cursor.fetchone()[0]
+    if id == None:
+        id = 1
+
+    # Calculate rewards
+    mana = 0
+    energy = 0
+    crystal = 0
+    if stage == 10:
+        energy = 50
+    elif stage == 20:
+        # Rainbowmon 3* MAX
+        insert(db, "run_drop_item", (id, 143140325, 1))
+    elif stage == 30:
+        # Mystical Scroll
+        insert(db, "run_drop_item", (id, 3, 1))
+    elif stage == 40:
+        crystal = 100
+    elif stage == 50:
+        # Mystical Scroll
+        insert(db, "run_drop_item", (id, 3, 2))
+    elif stage == 60:
+        # Rainbowmon 4* MAX
+        insert(db, "run_drop_item", (id, 143140430, 1))
+    elif stage == 70:
+        # Devilmon
+        insert(db, "run_drop_item", (id, 151050101, 1))
+    elif stage == 80:
+        crystal = 300
+    elif stage == 90:
+        # Light and Dark Scroll
+        insert(db, "run_drop_item", (id, 11, 1))
+    elif stage == 100:
+        # Legendary Scroll
+        insert(db, "run_drop_item", (id, 10, 1))
+    elif stage / 5 == 0:
+        crystal = 10 * int(math.ceil((stage / 2) / 10) + 1)
+    else:
+        # Summon stone
+        insert(db, "run_drop_item", (id, 8, 1))
+
+    # Insert
+    insert(db, "run", (uid, id, dtime, area_type, area, stage, difficulty, win, time, mana, energy, crystal, helper))
+
+    # Parse units
+    i = 0
+    for unit in data_request["unit_id_list"]:
+        unit_id = unit["unit_id"]
+        cursor.execute("SELECT unit FROM unit WHERE uid = ? AND id = ?;", (uid, unit_id))
+        unit_master_id = cursor.fetchone()[0]
+        for k_unit in data_response["unit_list"]:
+            if k_unit["unit_id"] == unit["unit_id"]:
+                unit_master_id = k_unit["unit_master_id"];
+                break;
+        if i == 0:
+            leader = 1 # TODO UNKNOWABLE? Check event start
+        else:
+            leader = 0
+        front = 0
+        insert(db, "run_party", (id, unit_master_id, unit_id, leader, front))
+        i += 1
+    db.commit()
+
+
+"""
+Begin script
+"""
+data_request = readData(2)
+data_response = readData(3)
+key = readKey()
+db = openDatabase()
+if verifyKey(db, data_response, key) == False:
+    print("Invalid API KEY...")
+    sys.exit(401)
+else:
+    try:
+        parseRun(db, data_request, data_response)
+    except Exception as e:
+        print("Error parsing TOA run: " + str(e))
+        sys.exit(400)
+sys.exit(201)
+
+

+ 0 - 0
public/API/v1/log-profile/index.php → application/API/v2/log-profile/index.php


+ 0 - 0
public/API/v1/log-run/index.php → application/API/v2/log-run/index.php


+ 17 - 0
application/API/v2/main.php

@@ -0,0 +1,17 @@
+<?php
+    header("Content-Type:application/json");
+    $links = [
+        "Archetypes" => URL::BASE . "/API/v2/archetypes",
+        "Area types" => URL::BASE . "/API/v2/area_types",
+        "Areas types" => URL::BASE . "/API/v2/areas",
+        "Buildings" => URL::BASE . "/API/v2/buildings",
+        "Decorations" => URL::BASE . "/API/v2/decorations",
+        "Areas types" => URL::BASE . "/API/v2/areas",
+        "Units" => URL::BASE . "/API/v2/units",
+        "Skills" => URL::BASE . "/API/v2/skills",
+        "Skill effects" => URL::BASE . "/API/v2/skill_efects",
+    ];
+    header("HTTP/1.1 200");
+    $json_response = json_encode($links);
+    echo $json_response;
+?>

+ 94 - 0
application/API/v2/upload_profile.php

@@ -0,0 +1,94 @@
+<?php
+    /**
+     * Profile uploader script.
+     *
+     * Exposes an API to save a run to the database.
+     * Reads post data and calls the upload_profile.py script.
+     * It also saves the data to a JSON file in the data directory.
+     * Mandatory POST parameters are:
+     *  - data: Received JSON file.
+     *  - key: User API key.
+     *
+     * @category API
+     */
+
+    global $db;
+
+    try{
+        // Check data
+        $data = filter_input(INPUT_POST, 'data');
+        if ($data == null || $data == false){
+            http_response_code(400);
+            return 400;
+        }
+
+        // Check API key.
+        $key = filter_input(INPUT_POST, 'key');
+        if ($key == null || $key == false){
+            http_response_code(401);
+            return 401;
+        }
+
+        // Check data format.
+        $json = json_decode($data);
+        if ($json === null){
+            http_response_code(400);
+            return 400;
+        }
+
+        // Authenticate
+        $uid = $json->{"wizard_id"};
+        $uname = $json->{"wizard_info"}->{"wizard_name"};
+        $s = "SELECT COUNT(uid) AS c FROM player WHERE uid = $uid AND api_key = '$key';";
+        if (1 != $db->query($s)->fetchArray(SQLITE3_ASSOC)["c"]){
+            http_response_code(401);
+            return 401;
+        }
+
+        // Write data file
+        $dtime = (new DateTime())->format('Y-m-dTH:i:s');
+        $fname = ($_SERVER["DOCUMENT_ROOT"] . "/../data/profile_" . $uid . "_" . $uname . "_" . $dtime . ".json");
+        try{
+            file_put_contents($fname, $data);
+        }
+        catch(Exception $e) {
+            error_log("Unable to write profile data to '$fname': " . $e->getMessage());
+            http_response_code(500);
+            return 500;
+        }
+
+        // Run profile parser script
+        $cmd = __DIR__ . "/bin/upload_profile.py " . $key . " " . $fname;
+        $out = [];
+        $ret = 0;
+        try{
+            exec($cmd, $out, $ret);
+        }
+        catch(Exception $e) {
+            error_log("Error running profile script '$cmd': " . $e->getMessage());
+            http_response_code(500);
+            return 500;
+        }
+
+        if ($ret != 200){
+            try{
+                http_response_code($ret);
+                return $ret;
+            }
+            catch(Exception $e) {
+                error_log("Profile script '$cmd' returned an unexpected value $ret: " . $e->getMessage());
+                http_response_code(500);
+                return 500;
+            }
+        }
+
+        // At this point, status code should be 200
+        http_response_code($ret);
+        return $ret;
+    }
+    catch(Exception $e) {
+        error_log("Unknown error parsing profile: " . $e->getMessage());
+        http_response_code(500);
+        return 500;
+    }
+?>

+ 83 - 0
application/API/v2/upload_run_dimension.php

@@ -0,0 +1,83 @@
+<?php
+    /**
+     * Dimension Hole Dungeon run logger script.
+     *
+     * Exposes an API to save a run to the database.
+     * Reads post data and calls the upload_run_dimension.py script.
+     * Mandatory POST parameters are:
+     *  - request: Intercepted game request.
+     *  - response: Intercepted game response.
+     *  - key: User API key.
+     *
+     * @category API
+     */
+
+    global $db;
+
+    try{
+        // Check data
+        $data_request = filter_input(INPUT_POST, 'request');
+        $data_response= filter_input(INPUT_POST, 'response');
+        if ($data_request == null || $data_request == false || $data_response == null || $data_response == false){
+            http_response_code(400);
+            return 400;
+        }
+
+        // Check API key.
+        $key = filter_input(INPUT_POST, 'key');
+        if ($key == null || $key == false){
+            http_response_code(401);
+            return 401;
+        }
+
+        // Check data format.
+        $json_request = json_decode($data_request);
+        $json_response = json_decode($data_response);
+        if ($json_response === null | $json_request === null){
+            http_response_code(400);
+            return 400;
+        }
+
+        // Authenticate
+        $uid = $json_request->{"wizard_id"};
+        $uname = $json_response->{"wizard_info"}->{"wizard_name"};
+        $s = "SELECT COUNT(uid) AS c FROM player WHERE uid = $uid AND api_key = '$key';";
+        if (1 != $db->query($s)->fetchArray(SQLITE3_ASSOC)["c"]){
+            http_response_code(401);
+            return 401;
+        }
+
+        // Run TOA run parser script
+        $cmd = __DIR__ . "/bin/upload_run_dimension.py " . $key . " " . escapeshellarg($data_request) . " " . escapeshellarg($data_response);
+        $out = [];
+        $ret = 0;
+        try{
+            exec($cmd, $out, $ret);
+        }
+        catch(Exception $e) {
+            error_log("Error running Dimension Hole Dungeon run script: " . $e->getMessage());
+            http_response_code(500);
+            return 500;
+        }
+
+        if ($ret != 201){
+            try{
+                http_response_code($ret);
+                return $ret;
+            }
+            catch(Exception $e) {
+                error_log("Dimension Hole Dungeon run script returned an unexpected value $ret: " . $e->getMessage());
+                http_response_code(500);
+                return 500;
+            }
+        }
+        // At this point, status code should be 200
+        http_response_code($ret);
+        return $ret;
+    }
+    catch(Exception $e) {
+        error_log("Unknown error parsing Dimension Hole Dungeon run: " . $e->getMessage());
+        http_response_code(500);
+        return 500;
+    }
+?>

+ 93 - 0
application/API/v2/upload_run_dungeon.php

@@ -0,0 +1,93 @@
+<?php
+    /**
+     * Dungeon run logger script.
+     *
+     * Exposes an API to save a run to the database.
+     * Reads post data and calls the upload_dungeon_run.py script.
+     * Mandatory POST parameters are:
+     *  - request: Intercepted game request.
+     *  - response: Intercepted game response.
+     *  - key: User API key.
+     *
+     * @category API
+     */
+
+    global $db;
+
+//foreach($_POST as $k => $v) {
+//    error_log(" POST $k: $v");
+//}
+
+
+    try{
+        // Check data
+        $data_request = filter_input(INPUT_POST, 'request');
+        $data_response= filter_input(INPUT_POST, 'response');
+        if ($data_request == null || $data_request == false || $data_response == null || $data_response == false){
+error_log("error1");
+            http_response_code(400);
+            return 400;
+        }
+
+        // Check API key.
+        $key = filter_input(INPUT_POST, 'key');
+        if ($key == null || $key == false){
+            http_response_code(401);
+            return 401;
+        }
+
+        // Check data format.
+        $json_request = json_decode($data_request);
+        $json_response = json_decode($data_response);
+        if ($json_response === null | $json_request === null){
+error_log("error2");
+            http_response_code(400);
+            return 400;
+        }
+
+        // Authenticate
+        $uid = $json_request->{"wizard_id"};
+        $uname = $json_response->{"wizard_info"}->{"wizard_name"};
+        $s = "SELECT COUNT(uid) AS c FROM player WHERE uid = $uid AND api_key = '$key';";
+        if (1 != $db->query($s)->fetchArray(SQLITE3_ASSOC)["c"]){
+            http_response_code(401);
+            return 401;
+        }
+
+        // Run dungeon run parser script
+        $cmd = __DIR__ . "/bin/upload_run_dungeon.py " . $key . " " . escapeshellarg($data_request) . " " . escapeshellarg($data_response);
+        $out = [];
+        $ret = 0;
+//error_log($cmd);
+        try{
+            exec($cmd, $out, $ret);
+        }
+        catch(Exception $e) {
+            error_log("Error running dungeon run script: " . $e->getMessage());
+            http_response_code(500);
+            return 500;
+        }
+
+        if ($ret != 201){
+            try{
+                http_response_code($ret);
+error_log("NO 201: $ret");
+                return $ret;
+            }
+            catch(Exception $e) {
+                error_log("Dungeon run script returned an unexpected value $ret: " . $e->getMessage());
+                http_response_code(500);
+                return 500;
+            }
+        }
+error_log("ACTUAL RET: " . $ret);
+        // At this point, status code should be 200
+        http_response_code($ret);
+        return $ret;
+    }
+    catch(Exception $e) {
+        error_log("Unknown error parsing dungeon run: " . $e->getMessage());
+        http_response_code(500);
+        return 500;
+    }
+?>

+ 81 - 0
application/API/v2/upload_run_scenario.php

@@ -0,0 +1,81 @@
+<?php
+    /**
+     * Scenario run logger script.
+     *
+     * Exposes an API to save a run to the database.
+     * Reads post data and calls the upload_scenario_run.py script.
+     * Mandatory POST parameters are:
+     *  - data: Received JSON file after a run.
+     *  - key: User API key.
+     *
+     * @category API
+     */
+
+    global $db;
+
+    try{
+        // Check data
+        $data = filter_input(INPUT_POST, 'data');
+        if ($data == null || $data == false){
+            http_response_code(400);
+            return 400;
+        }
+
+        // Check API key.
+        $key = filter_input(INPUT_POST, 'key');
+        if ($key == null || $key == false){
+            http_response_code(401);
+            return 401;
+        }
+
+        // Check data format.
+        $json = json_decode($data);
+        if ($json === null){
+            http_response_code(400);
+            return 400;
+        }
+
+        // Authenticate
+        $uid = $json->{"wizard_id"};
+        $uname = $json->{"wizard_info"}->{"wizard_name"};
+        $s = "SELECT COUNT(uid) AS c FROM player WHERE uid = $uid AND api_key = '$key';";
+        if (1 != $db->query($s)->fetchArray(SQLITE3_ASSOC)["c"]){
+            http_response_code(401);
+            return 401;
+        }
+
+        // Run scenario run parser script
+        $cmd = __DIR__ . "/bin/upload_run_scenario.py " . $key . " " . escapeshellarg($data);;
+        $out = [];
+        $ret = 0;
+        try{
+            exec($cmd, $out, $ret);
+        }
+        catch(Exception $e) {
+            error_log("Error running scenario run script '$cmd': " . $e->getMessage());
+            http_response_code(500);
+            return 500;
+        }
+
+        if ($ret != 200){
+            try{
+                http_response_code($ret);
+                return $ret;
+            }
+            catch(Exception $e) {
+                error_log("Dungeon run script '$cmd' returned an unexpected value $ret: " . $e->getMessage());
+                http_response_code(500);
+                return 500;
+            }
+        }
+
+        // At this point, status code should be 200
+        http_response_code($ret);
+        return $ret;
+    }
+    catch(Exception $e) {
+        error_log("Unknown error parsing scenario run: " . $e->getMessage());
+        http_response_code(500);
+        return 500;
+    }
+?>

+ 83 - 0
application/API/v2/upload_run_toa.php

@@ -0,0 +1,83 @@
+<?php
+    /**
+     * Dungeon run logger script.
+     *
+     * Exposes an API to save a run to the database.
+     * Reads post data and calls the upload_dungeon_run.py script.
+     * Mandatory POST parameters are:
+     *  - request: Intercepted game request.
+     *  - response: Intercepted game response.
+     *  - key: User API key.
+     *
+     * @category API
+     */
+
+    global $db;
+
+    try{
+        // Check data
+        $data_request = filter_input(INPUT_POST, 'request');
+        $data_response= filter_input(INPUT_POST, 'response');
+        if ($data_request == null || $data_request == false || $data_response == null || $data_response == false){
+            http_response_code(400);
+            return 400;
+        }
+
+        // Check API key.
+        $key = filter_input(INPUT_POST, 'key');
+        if ($key == null || $key == false){
+            http_response_code(401);
+            return 401;
+        }
+
+        // Check data format.
+        $json_request = json_decode($data_request);
+        $json_response = json_decode($data_response);
+        if ($json_response === null | $json_request === null){
+            http_response_code(400);
+            return 400;
+        }
+
+        // Authenticate
+        $uid = $json_request->{"wizard_id"};
+        $uname = $json_response->{"wizard_info"}->{"wizard_name"};
+        $s = "SELECT COUNT(uid) AS c FROM player WHERE uid = $uid AND api_key = '$key';";
+        if (1 != $db->query($s)->fetchArray(SQLITE3_ASSOC)["c"]){
+            http_response_code(401);
+            return 401;
+        }
+
+        // Run TOA run parser script
+        $cmd = __DIR__ . "/bin/upload_run_toa.py " . $key . " " . escapeshellarg($data_request) . " " . escapeshellarg($data_response);
+        $out = [];
+        $ret = 0;
+        try{
+            exec($cmd, $out, $ret);
+        }
+        catch(Exception $e) {
+            error_log("Error running TOA run script: " . $e->getMessage());
+            http_response_code(500);
+            return 500;
+        }
+
+        if ($ret != 201){
+            try{
+                http_response_code($ret);
+                return $ret;
+            }
+            catch(Exception $e) {
+                error_log("TOA run script returned an unexpected value $ret: " . $e->getMessage());
+                http_response_code(500);
+                return 500;
+            }
+        }
+        // At this point, status code should be 200
+        http_response_code($ret);
+        return $ret;
+    }
+    catch(Exception $e) {
+        error_log("Unknown error parsing TOA run: " . $e->getMessage());
+        http_response_code(500);
+        return 500;
+    }
+?>

+ 25 - 2
application/Controller.php

@@ -23,7 +23,7 @@
      * @global int $UID.
      * @global int $UID.
      */
      */
     $UID = 0;
     $UID = 0;
-    
+
     /**
     /**
      * @global resource $DB.
      * @global resource $DB.
      */
      */
@@ -68,9 +68,32 @@
                 }
                 }
             }
             }
 
 
+            // API call: Use API controller
+            if (count($pars) > 0 && strtoupper($pars[0]) == "API"){
+                if (count($pars) == 1){
+                    // TODO: API MAIN PAGE
+                }
+                else{
+                    $version = strtolower($pars[1]);
+                    $api_params = $pars;
+                    array_shift($api_params);
+                    array_shift($api_params);
+                    switch ($version){
+                        case "v1":
+                            require_once(__DIR__ . "/API/v1/API_Controller.php");
+                            new API_Controller($params);
+                            break;
+                        case "v2":
+                            require_once(__DIR__ . "/API/v2/API_Controller.php");
+                            new API_Controller($params);
+                            break;
+                    }
+                }
+                return;
+            }
             // Special case: Actions.
             // Special case: Actions.
             // Not redirecting to page, just execute a function
             // Not redirecting to page, just execute a function
-            if (count($pars) > 1 && strtoupper($pars[0]) == "ACTION"){
+            elseif (count($pars) > 1 && strtoupper($pars[0]) == "ACTION"){
                 switch (strtoupper($pars[1])){
                 switch (strtoupper($pars[1])){
                     case "LOGIN":
                     case "LOGIN":
                         require_once(PATH::ACTION . "login.php");
                         require_once(PATH::ACTION . "login.php");

+ 4 - 0
install_data/install_data.sql

@@ -158,6 +158,8 @@ CREATE TABLE run_party(
     run INT NOT NULL REFERENCES run(id),
     run INT NOT NULL REFERENCES run(id),
     unit INT NOT NULL REFERENCES unit(id),
     unit INT NOT NULL REFERENCES unit(id),
     k_unit INT NOT NULL REFERENCES k_unit(id),
     k_unit INT NOT NULL REFERENCES k_unit(id),
+    leader INT NOT NULL DEFAULT 0,
+    front INT NOT NULL DEFAULT 0,
     PRIMARY KEY (run, unit)
     PRIMARY KEY (run, unit)
 );
 );
 CREATE TABLE run_drop_rune_craft(
 CREATE TABLE run_drop_rune_craft(
@@ -200,6 +202,7 @@ CREATE TABLE run(
     uid INT NOT NULL REFERENCES player(id),
     uid INT NOT NULL REFERENCES player(id),
     id INT NOT NULL PRIMARY KEY,
     id INT NOT NULL PRIMARY KEY,
     dtime TIMESTAMP NOT NULL,
     dtime TIMESTAMP NOT NULL,
+    area_type INT NOT NULL REFERENCES k_area_type,
     area INT NOT NULL REFERENCES k_area,
     area INT NOT NULL REFERENCES k_area,
     stage INT CHECK(stage > 0),
     stage INT CHECK(stage > 0),
     difficulty INT REFERENCES k_difficulty(id),
     difficulty INT REFERENCES k_difficulty(id),
@@ -220,6 +223,7 @@ CREATE TABLE run_drop_rune(
     quality INT NOT NULL REFERENCES k_quality(id),
     quality INT NOT NULL REFERENCES k_quality(id),
     value INT NOT NULL CHECK(value > 0),
     value INT NOT NULL CHECK(value > 0),
     efficiency FLOAT NOT NULL CHECK(efficiency BETWEEN 0 AND 100),
     efficiency FLOAT NOT NULL CHECK(efficiency BETWEEN 0 AND 100),
+    max_efficiency FLOAT NOT NULL CHECK(max_efficiency BETWEEN 0 AND 100),
     main_stat INT NOT NULL REFERENCES k_rune_stat(id),
     main_stat INT NOT NULL REFERENCES k_rune_stat(id),
     main_stat_value INT NOT NULL CHECK(value > 0),
     main_stat_value INT NOT NULL CHECK(value > 0),
     innate_stat INT REFERENCES k_rune_stat(id),
     innate_stat INT REFERENCES k_rune_stat(id),