Bladeren bron

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 jaren geleden
bovenliggende
commit
d2928213af

+ 1 - 0
.gitignore

@@ -7,6 +7,7 @@ data/*.sqlite
 .php_pid
 *.log
 doc/.tmp
+application/API/v2/bin/__pycache__/
 
 #IDE / OS files
 .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.
      */
     $UID = 0;
-    
+
     /**
      * @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.
             // 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])){
                     case "LOGIN":
                         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),
     unit INT NOT NULL REFERENCES 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)
 );
 CREATE TABLE run_drop_rune_craft(
@@ -200,6 +202,7 @@ CREATE TABLE run(
     uid INT NOT NULL REFERENCES player(id),
     id INT NOT NULL PRIMARY KEY,
     dtime TIMESTAMP NOT NULL,
+    area_type INT NOT NULL REFERENCES k_area_type,
     area INT NOT NULL REFERENCES k_area,
     stage INT CHECK(stage > 0),
     difficulty INT REFERENCES k_difficulty(id),
@@ -220,6 +223,7 @@ CREATE TABLE run_drop_rune(
     quality INT NOT NULL REFERENCES k_quality(id),
     value INT NOT NULL CHECK(value > 0),
     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_value INT NOT NULL CHECK(value > 0),
     innate_stat INT REFERENCES k_rune_stat(id),