Pārlūkot izejas kodu

New API version 3. GET, POST, PUT and DELETE methods for profile implemented. POST method for Cairos Dngeon runs implemented. SWEX plugin updated to work with the new profile and Cairos run importer.

Iñigo Valentin 5 gadi atpakaļ
vecāks
revīzija
62a0dbe878

+ 97 - 0
application/API/v3/API_Controller.php

@@ -0,0 +1,97 @@
+<?php
+
+    /**
+     * v3 API Controller file.
+     *
+     * Provides a class to handle all posible API requests.
+     *
+     * @category Constroller
+     */
+
+    /**
+     * v3 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[] $query GET parameters of the request.
+         */
+        public function __construct($query){
+
+            // Remove host
+            array_shift($query);
+            // Remove /API/
+            array_shift($query);
+            // Remove /v3/
+            array_shift($query);
+            // API call: Use API controller
+            $method = $_SERVER["REQUEST_METHOD"];
+            if (!in_array($method, ["GET", "POST", "PUT", "DELETE"])){
+                $method = "GET";
+            }
+            if (count($query) == 0){
+                require_once(__DIR__ . "main.php");
+            }
+            else{
+                $command = strtolower($query[0]);
+                // Remove /<command>/
+                array_shift($query);
+                switch ($command){
+                    case "help":
+                        require_once(__DIR__ . "/help/index.php");
+                        break;
+                    case "profile":
+                    case "run":
+                    case "logbook":
+                    case "collection":
+                    case "run":
+                        if (file_exists(__DIR__ . "/$command/$method.php")){
+                            require_once(__DIR__ . "/$command/$method.php");
+                        }
+                        else{
+                            header("HTTP/1.1 404");
+                        }
+                        break;
+                    /*case "update_profile":
+                        require_once(__DIR__ . "/update_profile.php");
+                        break;
+                    case "update_logbook":
+                        require_once(__DIR__ . "/update_logbook.php");
+                        break;
+                    case "update_collection":
+                        require_once(__DIR__ . "/update_collection.php");
+                        break;
+                    case "log_run_dungeon":
+                        require_once(__DIR__ . "/log_run_dungeon.php");
+                        break;
+                    case "log_run_dh":
+                        require_once(__DIR__ . "/log_run_dh.php");
+                        break;
+                    case "log_run_scenario":
+                        require_once(__DIR__ . "/log_run_scenario.php");
+                        break;
+                    case "log_run_toa":
+                        require_once(__DIR__ . "/log_run_toa.php");
+                        break;
+                    case "log_run_rift_dungeon":
+                        require_once(__DIR__ . "/log_run_rift_dungeon.php");
+                        break;
+                    case "units":
+                        require_once(__DIR__ . "/units.php");
+                        break;*/
+                    default:
+                        header("HTTP/1.1 404");
+                }
+            }
+        }
+    }
+?>

+ 348 - 0
application/API/v3/help/api.php

@@ -0,0 +1,348 @@
+<?php
+
+    $API = [
+        "version" => 2,
+        "category" => [
+            [ // BEGIN CATEGORY Profile
+                "name" => "Profile",
+                "command" => [
+                    [ // BEGIN COMMAND UploadProfile
+                        "name" => "profile",
+                        "method" => [
+                            [
+                                "id" => "GET",
+                                "url" => "/profile/{id}/",
+                                "description" => "Retrieves player profile.",
+                                "parameters" => [
+                                    [
+                                        "key" => "{id}",
+                                        "via" => "query",
+                                        "type" => "string",
+                                        "description" => "Player ID or name.",
+                                        "optional" => false
+                                    ],
+                                    [
+                                        "key" => "key",
+                                        "via" => "body",
+                                        "type" => "string",
+                                        "description" => "API key. Required for private profiles.",
+                                        "optional" => true
+                                    ]
+                                ],
+                                "responses" => [
+                                    [
+                                        "status" => "200",
+                                        "message" => "Success.",
+                                        "description" => null,
+                                        "example" => json_encode('{"username":"IValentin","level":50,"public":"1"}', JSON_PRETTY_PRINT),
+                                    ],
+                                    [
+                                        "status" => "403",
+                                        "message" => "A list of profiles cant be retrieved. Please specify a profile ID or player name.",
+                                        "description" => "No player ID or name has been supplied. A full list of profiles is not available.",
+                                        "example" => null
+                                    ],
+                                    [
+                                        "status" => "403",
+                                        "message" => "The profile is not public.",
+                                        "description" => "The requestd player profile is private, and the API key has not been sent or it's invalid.",
+                                        "example" => null
+                                    ],
+                                    [
+                                        "status" => "404",
+                                        "message" => "Player not found.",
+                                        "description" => "The requestd player is not registered in SWDB.",
+                                        "example" => null
+                                    ]
+                                ]
+                            ],
+                            [
+                                "id" => "POST",
+                                "url" => "/profile/{id}/",
+                                "description" => "Creates a new player profile.",
+                                "parameters" => [
+                                    [
+                                        "key" => "{id}",
+                                        "via" => "query",
+                                        "type" => "string",
+                                        "description" => "Player ID.",
+                                        "optional" => false
+                                    ],
+                                    [
+                                        "key" => "email",
+                                        "via" => "body",
+                                        "type" => "string",
+                                        "description" => "User email. Must be a valid address.",
+                                        "optional" => false
+                                    ],
+                                    [
+                                        "key" => "password",
+                                        "via" => "body",
+                                        "type" => "string",
+                                        "description" => "New user password.",
+                                        "optional" => false
+                                    ],
+                                    [
+                                        "key" => "name",
+                                        "via" => "body",
+                                        "type" => "string",
+                                        "description" => "New username. If not supplied, it will default to 'player_[id]'. Will be overriden on the next PUT request.",
+                                        "optional" => true
+                                    ],
+                                    [
+                                        "key" => "public",
+                                        "via" => "body",
+                                        "type" => "int",
+                                        "description" => "1 to make the profile public, 0 to keep it private. Default to 0.",
+                                        "optional" => true
+                                    ],
+                                ],
+                                "responses" => [
+                                    [
+                                        "status" => "201",
+                                        "message" => "Created.",
+                                        "description" => null,
+                                        "example" => json_encode('{"username":"IValentin","email":example@example.com,"api_key":"0123456789012345","public":"0"}', JSON_PRETTY_PRINT),
+                                    ],
+                                    [
+                                        "status" => "400",
+                                        "message" => "User ID not received.",
+                                        "description" => "The ID of the player to create is not in the query.",
+                                        "example" => null
+                                    ],
+                                    [
+                                        "status" => "400",
+                                        "message" => "User email not received.",
+                                        "description" => "The email of the player to create has not ben passed in the request.",
+                                        "example" => null
+                                    ],
+                                    [
+                                        "status" => "400",
+                                        "message" => "Invalid email.",
+                                        "description" => "The 'mail' parameter must be a valid email address.",
+                                        "example" => null
+                                    ],
+                                    [
+                                        "status" => "400",
+                                        "message" => "User password not received.",
+                                        "description" => "The password of the player to create has not ben passed in the request.",
+                                        "example" => null
+                                    ],
+                                    [
+                                        "status" => "400",
+                                        "message" => "Existing user.",
+                                        "description" => "A player with the same ID is already registered in SWDB.",
+                                        "example" => null
+                                    ]
+                                ]
+                            ],
+                            [
+                                "id" => "PUT",
+                                "url" => "/profile/{id}/",
+                                "description" => "Updates a player profile with data received from the game.",
+                                "parameters" => [
+                                    [
+                                        "key" => "{id}",
+                                        "via" => "query",
+                                        "type" => "string",
+                                        "description" => "Player ID.",
+                                        "optional" => false
+                                    ],
+                                    [
+                                        "key" => "key",
+                                        "via" => "body",
+                                        "type" => "string",
+                                        "description" => "API key.",
+                                        "optional" => false
+                                    ],
+                                    [
+                                        "key" => "response",
+                                        "via" => "body",
+                                        "type" => "string",
+                                        "description" => "JSON received from game server on HubUserLogin command.",
+                                        "optional" => false
+                                    ]
+                                ],
+                                "responses" => [
+                                    [
+                                        "status" => "204",
+                                        "message" => "Profile imported.",
+                                        "description" => null,
+                                        "example" => null,
+                                    ],
+                                    [
+                                        "status" => "400",
+                                        "message" => "API key not received.",
+                                        "description" => "No API key was suplied in the request.",
+                                        "example" => null
+                                    ],
+                                    [
+                                        "status" => "400",
+                                        "message" => "No data received.",
+                                        "description" => "Game JSON response not received from request",
+                                        "example" => null
+                                    ],
+                                    [
+                                        "status" => "400",
+                                        "message" => "Data is no valid JSON.",
+                                        "description" => "The parameter 'response' contains information, but it's not valid JSON data.",
+                                        "example" => null
+                                    ],
+                                    [
+                                        "status" => "401",
+                                        "message" => "Invalid credentials",
+                                        "description" => "The API key is not valid for the profile being updated.",
+                                        "example" => null
+                                    ]
+                                ]
+                            ],
+                            [
+                                "id" => "DELETE",
+                                "url" => "/profile/{id}/",
+                                "description" => "Deletes a player profile.",
+                                "parameters" => [
+                                    [
+                                        "key" => "{id}",
+                                        "via" => "query",
+                                        "type" => "string",
+                                        "description" => "Player ID.",
+                                        "optional" => false
+                                    ],
+                                    [
+                                        "key" => "key",
+                                        "via" => "body",
+                                        "type" => "string",
+                                        "description" => "API key.",
+                                        "optional" => false
+                                    ]
+                                ],
+                                "responses" => [
+                                    [
+                                        "status" => "204",
+                                        "message" => "Profile deleted.",
+                                        "description" => null,
+                                        "example" => null,
+                                    ],
+                                    [
+                                        "status" => "400",
+                                        "message" => "User ID not received.",
+                                        "description" => "The ID of the player to create is not in the query.",
+                                        "example" => null
+                                    ],
+                                    [
+                                        "status" => "400",
+                                        "message" => "API key not received.",
+                                        "description" => "No API key was suplied in the request.",
+                                        "example" => null
+                                    ],
+                                    [
+                                        "status" => "401",
+                                        "message" => "Invalid credentials",
+                                        "description" => "The API key is not valid for the profile being updated.",
+                                        "example" => null
+                                    ]
+                                ]
+                            ],
+                        ],
+                    ] // END COMMAND profile
+                ]
+            ], // END CATEGORY Profile
+            [ // BEGIN CATEGORY Run
+                "name" => "Run",
+                "command" => [
+                    [ // BEGIN COMMAND run
+                        "name" => "run",
+                        "method" => [
+                            [
+                                "id" => "POST",
+                                "url" => "/run/",
+                                "description" => "Logs a game run.",
+                                "parameters" => [
+                                    [
+                                        "key" => "key",
+                                        "via" => "body",
+                                        "type" => "string",
+                                        "description" => "API key.",
+                                        "optional" => false
+                                    ],
+                                    [
+                                        "key" => "result_response",
+                                        "via" => "body",
+                                        "type" => "string",
+                                        "description" => "JSON received from game server on run result.",
+                                        "optional" => false
+                                    ],
+                                    [
+                                        "key" => "result_request",
+                                        "via" => "body",
+                                        "type" => "string",
+                                        "description" => "JSON sent to game server on run result. Required for commands: BattleDungeonResult_V2",
+                                        "optional" => true
+                                    ],
+                                    [
+                                        "key" => "start_response",
+                                        "via" => "body",
+                                        "type" => "string",
+                                        "description" => "JSON received from game server on run start. Required for commands: ",
+                                        "optional" => true
+                                    ],
+                                    [
+                                        "key" => "start_request",
+                                        "via" => "body",
+                                        "type" => "string",
+                                        "description" => "JSON sent to game server on run start. Required for commands: ",
+                                        "optional" => true
+                                    ],
+                                ],
+                                "responses" => [
+                                    [
+                                        "status" => "201",
+                                        "message" => "Created.",
+                                        "description" => null,
+                                        "example" => null,
+                                    ],
+                                    [
+                                        "status" => "400",
+                                        "message" => "API key not received.",
+                                        "description" => "No API key was suplied in the request.",
+                                        "example" => null
+                                    ],
+                                    [
+                                        "status" => "400",
+                                        "message" => "No result response data received.",
+                                        "description" => "JSON received from game server on run result wast receiven on the request.",
+                                        "example" => null
+                                    ],
+                                    [
+                                        "status" => "400",
+                                        "message" => "Result response data is no valid JSON.",
+                                        "description" => "The parameter 'result_response' contains information, but it's not valid JSON data.",
+                                        "example" => null
+                                    ],
+                                    [
+                                        "status" => "401",
+                                        "message" => "Invalid credentials",
+                                        "description" => "The API key is not valid for the profile being updated.",
+                                        "example" => null
+                                    ],
+                                    [
+                                        "status" => "405",
+                                        "message" => "Command not implemented.",
+                                        "description" => "The command referenced in the JSON file is not implemented.",
+                                        "example" => null
+                                    ],
+                                    [
+                                        "status" => "409",
+                                        "message" => "Run already in database.",
+                                        "description" => "The run is already in the database for the user.",
+                                        "example" => null
+                                    ]
+                                ]
+                            ],
+                        ],
+                    ] // END COMMAND Run
+                ]
+            ], // END CATEGORY Run
+        ],
+    ];
+?>

+ 537 - 0
application/API/v3/help/index.php

@@ -0,0 +1,537 @@
+<?php
+    /**
+     * API Help view.
+     *
+     * Shows API documentation.
+     *
+     * @category View
+     */
+?>
+<?php
+    include __DIR__ . "/api.php";
+?>
+<!DOCTYPE html>
+<html lang='en'>
+    <head>
+        <meta content='text/html; charset=utf-8' http-equiv='content-type'/>
+        <meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1'/>
+        <title>SWDB APIv3</title>
+        <link rel='shortcut icon' href='<?=URL::IMG["LOGO"]?>sw.png'/>
+        <!-- CSS -->
+        <link rel='stylesheet' type='text/css' href='<?=URL::CSS?>ui.css'/>
+        <style>
+
+            aside{
+                /*text-align: left;
+                position: fixed;
+                top: 0;
+                left: 0;
+                width: 15%;
+                height: 100%;
+                border-radius: 0;
+                margin: 0 1em 0 0;
+                padding: 0em 0.4em 0.1em 0.4em;
+                border-left: 0;
+                border-top: 0;
+                border-bottom: 0;*/
+            }
+
+            aside hr{
+                height: 0em;
+                padding: 0;
+                margin: 0 1em;
+                border: 0.01em solid #db490054;
+                border-radius: 1em;
+            }
+
+            aside h1{
+                /*text-align: center;
+                margin-top: 0;*/
+            }
+
+            aside h1 img{
+                /*max-width: 100%;
+                display: block;
+                margin: auto auto -1em auto;*/
+            }
+
+            aside h1 div{
+                /*text-shadow: 0 0 0.5em #000000, 0 0 0.5em #000000, 0 0 0.2em #000000, 0 0 0.2em #000000;
+                font-family: monospace, monospace;*/
+            }
+
+            aside h1 div#swdb{
+                /*font-weight: bold;
+                font-size: 160%;
+                margin-bottom: -0.2em;*/
+            }
+
+            aside h1 div#api{
+                /*font-style: italic;
+                font-size: 110%;*/
+            }
+
+            aside h3 {
+                margin: 0.4em 0 0.2em 1em;
+                border-left: 0.5em solid #db490054;
+                border-radius: 0.2em;
+                padding-left: 0.3em;
+                background-color: #00000000;
+                transition: all .3s ease-in-out;
+            }
+
+            aside h3:hover{
+                background-color: #db490054;
+            }
+
+            aside h3 a{
+                color: #cccccc
+            }
+
+            aside h3:hover a{
+                color: #ffffff
+            }
+
+            aside ul {
+                margin: 0.2em 0;
+                list-style: none;
+            }
+
+            aside ul li{
+                border-left: 0.5em solid #db490054;
+                border-radius: 0.2em;
+                padding-left: 0.3em;
+                margin-bottom: 0.2em;
+                background-color: #00000000;
+                transition: all .3s ease-in-out;
+            }
+
+            aside ul li:hover{
+                background-color: #db490054;
+            }
+
+            aside ul li a{
+                color: #cccccc
+            }
+
+            aside ul li:hover a{
+                color: #ffffff
+            }
+
+            aside div#copyright{
+                text-align: center;
+                font-size: 70%;
+                position: absolute;
+                bottom: 1em;
+            }
+
+            section.category{
+                /*margin: 2em 1em 1em 18%;*/
+            }
+
+            section.category h2{
+                /*padding: 0.5em 3em;*/
+            }
+
+            section.category article{
+                margin: 1.5em;
+                padding-left: 1em;
+                padding-right: 1em;
+                text-align: left;
+            }
+
+            section.category article h3{
+                vertical-align: bottom;
+            }
+
+            section.category article h3 span.description{
+                font-style: italic;
+                font-size: 90%;
+            }
+
+            section.category article:not(:last-child){
+                padding-bottom: 1em;
+                margin-bottom: 1em;
+                border-bottom: 0.1em solid #ffffff66;
+            }
+
+            section.category article div.command span{
+                display: inline-block;
+                border-radius: 0.6em;
+                padding: 0.3em 0.5em;
+                border-width: 0.15em;
+                border-style: solid;
+                color: #ffffff;
+                font-family: monospace, monospace;
+                font-weight: bold;
+            }
+
+            section.category article div.command span.url{
+                background-color: #292b36;
+                border-color: #4b4d57;
+            }
+
+            section.category article div.command span.type{
+                text-transform: uppercase;
+            }
+
+            section.category article div.command span.type_GET {
+                background-color: #008000;
+                border-color: #22a222;
+            }
+
+            section.category article div.command span.type_PUT {
+                background-color: #e5c500;
+                border-color: #f7e722;
+            }
+
+            section.category article div.command span.type_POST {
+                background-color: #4070ec;
+                border-color: #6292fe;
+            }
+
+            section.category article div.command span.type_DELETE {
+                background-color: #ed0039;
+                border-color: #ff225b;
+            }
+
+            section.category article table{
+                border-collapse: collapse;
+                width: 80%;
+                border-radius: 0.5em;
+                margin: 0.5em auto 1em auto;
+            }
+
+            section.category article table th {
+                background-color: #331100;
+                text-align: left;
+                font-weight: bold;
+                padding: 0.5em 1em;
+                border: 0.1px solid #e0e0e0;
+            }
+
+            section.category article table td {
+                background-color: #352413;
+                vertical-align: top;
+                padding: 0.2em 0.4em;
+                border: #e0e0e0 1px solid;
+            }
+
+            section.category article table td.name {
+                font-family: monospace, monospace;
+                font-weight: bold;
+            }
+
+            section.category article table td.name span.location{
+                padding-left: 1em;
+                font-size: 80%;
+                font-style: italic;
+                color: #ffffff77;
+            }
+
+            section.category article table td.type {
+                font-family: monospace, monospace;
+                font-weight: bold;
+            }
+
+            section.category article table td.status span{
+                display: inline-block;
+                border-radius: 0.6em;
+                padding: 0.3em 0.5em;
+                border-width: 0.15em;
+                border-style: solid;
+                color: #ffffff;
+                font-family: monospace, monospace;
+                font-weight: bold;
+            }
+
+            section.category article table td.status_1 span{
+                border-color: #999999;
+                background-color: #777777;
+            }
+            section.category article table td.status_2 span{
+                background-color: #008000;
+                border-color: #22a222;
+            }
+            section.category article table td.status_3 span{
+                background-color: #4070ec;
+                border-color: #6292fe;
+            }
+            section.category article table td.status_4 span{
+                background-color: #e5c500;
+                border-color: #f7e722;
+            }
+            section.category article table td.status_5 span{
+                background-color: #ed0039;
+                border-color: #ff225b;
+            }
+
+            section.category article table td.message{
+                font-family: monospace, monospace;
+            }
+
+            section.category article table td.description span.optional {
+                font-size: 90%;
+                color: #bbbbbb;
+                font-style: italic;
+            }
+
+            section#example{
+                position: fixed;
+                display: none;
+                top: 15%;
+                left: 20%;
+                width: 60%;
+                max-width: 90em;
+                height: 70%;
+                max-height: 30em;
+                text-align: right;
+            }
+
+            section#example article{
+                text-align: right;
+            }
+
+            section#example article pre#example_content{
+                text-align: left;
+                background-color: #292b36;
+                border: 0.3em solid #4b4d57;
+                border-radius: 0.8em;
+                padding: 1em;
+                max-height: 20em;
+                overflow: scroll;
+            }
+
+        </style>
+        <!-- Javascript -->
+        <script>
+            function showExample(command, status, example){
+                document.getElementById('example_title').innerHTML = command + " HTTP status " + status + " response example";
+                document.getElementById('example_content').innerHTML = JSON.stringify(JSON.parse(example), null, 4);
+                document.getElementById('example').style.display = 'block';
+            }
+            function closeExample(command, status, example){
+                document.getElementById('example').style.display = 'none';
+            }
+        </script>
+        <!-- Meta tags -->
+        <link rel='canonical' href='<?=URL::BASE?>API/v3/help/'/>
+        <link rel='author' href='SWDB'/>
+        <link rel='publisher' href='SWDB'/>
+        <meta name='description' content='SWDB API v3 Documentation'/>
+        <meta property='og:title' content='SWDB APIv3'/>
+        <meta property='og:url' content='<?=URL::BASE?>API/v3/help/'/>
+        <meta property='og:description' content='SWDB API v3 Documentation'/>
+        <meta property='og:image' content='<?=URL::IMG["LOGO"]?>sw.png'/>
+        <meta property='og:site_name' content='SWDB'/>
+        <meta property='og:type' content='website'/>
+        <meta property='og:locale' content='en'/>
+        <meta name='twitter:card' content='summary'/>
+        <meta name='twitter:title' content='SWDB APIv3'/>
+        <meta name='twitter:description' content='SWDB API v3 Documentation'/>
+        <meta name='twitter:image' content='<?=URL::IMG["LOGO"]?>sw.png'/>
+        <meta name='twitter:url' content='<?=URL::BASE?>API/v3/help/'/>
+        <meta name='robots' content='index follow'/>
+    </head>
+    <body>
+        <header>
+            <div id='logo'>
+                <img id='logo_img' src='<?=URL::IMG["LOGO"]?>sw.png' alt=''/>
+                <h1 id='logo_text'>
+                    <span class='large'>D</span>
+                    <span class='small'>ata</span>
+                    <span class='large'>B</span>
+                    <span class='small'>ase</span>
+                </h1>
+            </div>
+        </header>
+        <aside>
+            <h2>
+                API v3 endopoints:
+            </h2>
+<?php
+            foreach ($API["category"] as $category){
+?>
+                <h3>
+                    <a href='#category_<?=$category["name"]?>'>
+                        <?=$category["name"]?>
+                    </a>
+                </h3>
+                <ul>
+<?php
+                    foreach ($category["command"] as $command){
+                        foreach ($command["method"] as $method){
+?>
+                            <li>
+                                <a href='#command_<?=$command["name"]?>-<?=$method["id"]?>-<?=htmlspecialchars($method["url"])?>' alt='<?=$command["description"]?>'>
+                                    <?=$method["id"]?> <?=htmlspecialchars($method["url"])?>
+                                </a>
+                            </li>
+<?php
+                        }
+                    }
+?>
+                </ul>
+                <hr/>
+<?php
+            }
+?>
+        </aside>
+        <main>
+<?php
+            foreach ($API["category"] as $category){
+                foreach ($category["command"] as $command){
+?>
+                    <section id='category_<?=$category["name"]?>' class='category content'>
+                    <h2>
+                        <?=$category["name"]?>
+                    </h2>
+                        <article id='command_<?=$command["name"]?>'>
+                            <h3>
+                                <?=$command["name"]?>
+                            </h3>
+<?php
+                            foreach ($command["method"] as $method){
+?>
+                                <div class='command' id='command_<?=$command["name"]?>-<?=$method["id"]?>-<?=htmlspecialchars($method["url"])?>'>
+                                    <span class='type type_<?=$method["id"]?>'>
+                                        <?=$method["id"]?>
+                                    </span>
+                                    <span class='url'>
+                                        <?=htmlspecialchars($method["url"])?>
+                                    </span>
+                                    <span class='description'>
+                                        <?=$method["description"]?>
+                                    </span>
+                                </div>
+                                <table class='params'>
+                                    <thead>
+                                        <tr>
+                                            <th class='header' colspan='3'>
+                                                Parameters
+                                            </th>
+                                        </tr>
+                                        <tr>
+                                            <th class='name'>
+                                                Parameter
+                                            </th>
+                                            <th class='type'>
+                                                Type
+                                            </th>
+                                            <th class='description'>
+                                                Description
+                                            </th>
+                                        </tr>
+                                    </thead>
+                                    <tbody>
+    <?php
+                                        foreach ($method["parameters"] as $param){
+    ?>
+                                            <tr>
+                                                <td class='name'>
+                                                    <?=$param["key"]?>
+                                                    <span class='location'>
+                                                        (<?=$param["via"]?>)
+                                                    </span>
+                                                </td>
+                                                <td class='type'>
+                                                    <?=$param["type"]?>
+                                                </td>
+                                                <td class='description'>
+                                                    <?=($param["optional"] ? "<span class='optional'>[Optional] </span>" : "")?>
+                                                    <?=$param["description"]?>
+                                                </td>
+                                            </tr>
+    <?php
+                                        }
+    ?>
+                                    </tbody>
+                                </table>
+                                <table class='responses'>
+                                    <thead>
+                                        <tr>
+                                            <th class='header' colspan='3'>
+                                                Responses
+                                            </th>
+                                        </tr>
+                                        <tr>
+                                            <th class='status'>
+                                                Status
+                                            </th>
+                                            <th class='status'>
+                                                Message
+                                            </th>
+                                            <th class='description'>
+                                                Description
+                                            </th>
+                                        </tr>
+                                    </thead>
+                                    <tbody>
+    <?php
+                                        foreach ($method["responses"] as $response){
+    ?>
+                                            <tr>
+                                                <td class='status status_<?=substr($response["status"], 0, 1)?>'>
+                                                    <span>
+                                                        <?=$response["status"]?>
+                                                    </span>
+                                                </td>
+                                                <td class='message'>
+                                                    <?=$response["message"]?>
+                                                </td>
+                                                <td class='description'>
+                                                    <?=$response["description"]?>
+    <?php
+                                                    if ($response["example"] != null){
+    ?>
+                                                        <span class='fake_a' onclick="showExample('<?=$command["name"]?>', <?=$status["status"]?>, '<?=htmlspecialchars(json_decode($status["example"]), ENT_QUOTES)?>')">
+                                                            View example.
+                                                        </span>
+    <?php
+                                                    }
+    ?>
+                                                </td>
+                                            </tr>
+    <?php
+                                        }
+    ?>
+                                    </tbody>
+                                </table>
+<?php
+                            }
+?>
+                        </article>
+                    </section>
+<?php
+                }
+?>
+                <section id='example' class='content'>
+                    <h2 id='example_title'>
+                    </h2>
+                    <article>
+                        <pre id='example_content'>
+                        </pre>
+                        <input type='button' value='Close' onClick='closeExample();'>
+                    </article>
+                </section>
+<?php
+            }
+?>
+        </main>
+        <footer>
+            <table id='footer_table'>
+                <tr>
+                    <td id='footer_left'>
+                        Version <?=APP::VERSION?>. Developed by <a href='https://inigovalentin.com'>I&ntilde;igo Valentin</a>.
+                        <br/>
+                        <br/>
+                        Source code available on <a href='https://github.com'>Github</a> under the GPLv3.
+                    </td>
+                    <td id='footer_right'>
+                        Neither I nor this tool are affiliated with or endorsed by <a href='http://com2us.com/'>Com2uS</a> in any way. The Summoners >
+                    </td>
+                </tr>
+            </table>
+        </footer>
+    </body>
+</html>

+ 17 - 0
application/API/v3/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;
+?>

+ 76 - 0
application/API/v3/profile/DELETE.php

@@ -0,0 +1,76 @@
+<?php
+    /**
+     * Profile deleter script.
+     *
+     * Exposes an API to update an existing profile.
+     *
+     * It also saves the data to a JSON file in the data directory.
+     *
+     * Mandatory query parameters are:
+     *  - id: Player ID.
+     *
+     * Mandatory DELETE parameters are:
+     *  - key: User API key.
+     *
+     * @category API
+     */
+
+    global $db;
+
+    try{
+
+        header("Content-type: application/json; charset=utf-8");
+
+        // Get put data
+        parse_str(file_get_contents("php://input"), $_DELETE);
+
+        // Get profile ID
+        // $query heredated from API_CONTROLLER
+        if (count($query) > 0){
+            $id = $query[0];
+        }
+        else{
+            // ID is mandatory
+            header("HTTP/1.1 400 User ID not received.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 400 User ID not received.");
+            return 400;
+        }
+
+        // Check API key.
+        $api_key = $_DELETE["key"];
+        if ($api_key == null || $api_key == false){
+            header("HTTP/1.1 400 API key not received.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 400 API key not received.");
+            return 400;
+        }
+
+        // Authenticate
+        $statement = $db->prepare("SELECT COUNT(uid) AS c FROM player WHERE uid = :uid AND api_key = :api_key;");
+        $statement->bindValue(":uid", $uid);
+        $statement->bindValue(":api_key", $api_key);
+        if (1 != $statement->execute()->fetchArray(SQLITE3_ASSOC)["c"]){
+            header("HTTP/1.1 401 Invalid credentials.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 401 Invalid credentials.");
+            return 401;
+        }
+
+        $statement = $db->prepare("
+          DELETE FROM player
+          WHERE
+            uid = :uid AND
+            api_key, = :api_key;
+        ");
+        $statement->bindValue(":uid", $uid);
+        $statement->bindValue(":api_key", $api_key);
+        $statement->execute();
+
+        header("HTTP/1.1 204 Profile deleted.");
+        syslog(LOG_INFO, "[APIv3] HTTP/1.1 204 Profile deleted.");
+        return 204;
+    }
+    catch(Exception $e) {
+        header("HTTP/1.1 500 Unexpeced error: " . $e->getMessage());
+        syslog(LOG_ERR, "[APIv3] HTTP/1.1 500 Unexpected error: " . $e->getMessage());
+        return 500;
+    }
+?>

+ 67 - 0
application/API/v3/profile/GET.php

@@ -0,0 +1,67 @@
+<?php
+    /**
+     * Profile getter script.
+     *
+     * Exposes an API to rerieve a user profile.
+     *
+     * A lst of profiles can't be retrieved.
+     *
+     * Mandatory query parameters are:
+     *  - id: Player ID.
+     *
+     * Optional GET parameters are.
+     *  - key: User API key to retrieve a private profile.
+     *
+     * @category API
+     */
+
+    global $db;
+
+    try{
+        header("Content-type: application/json; charset=utf-8");
+
+        // Get profile ID
+        // $query heredated from API_CONTROLLER
+        if (count($query) > 1){
+            $id = $query[0];
+        }
+        else{
+            // Id is mandatory, a list can be retrieved
+            header("HTTP/1.1 403 A list of profiles cant be retrieved. Please specify a profile ID or player name.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 403 A list of profiles cant be retrieved. Please specify a profile ID or player name.");
+            return 403;
+        }
+
+        // Get player
+        $statement = $db->prepare("SELECT * FROM player WHERE uid = :uid OR upper(name) = upper(:uid);");
+        $statement->bindValue(":uid", $id);
+        $r = $statement->execute()->fetchArray(SQLITE3_ASSOC);
+        if ($r == null){
+            // No player found
+            header("HTTP/1.1 404 Player not found.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 404 Player not found.");
+            return 403;
+        }
+        if ($r["public"] != 1 && $r["key"] != filter_input(INPUT_GET, "key")){
+            header("HTTP/1.1 403 The profile is not public.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 403 The profile is not public.");
+            return 403;
+        }
+
+        // Build array
+        $player = [
+            "username" => $r["name"],
+            "level" => $r["level"],
+            "public" => "1"
+        ];
+        echo(json_encode($player));
+        header("HTTP/1.1 200 Success.");
+        syslog(LOG_INFO, "[APIv3] HTTP/1.1 200 Success.");
+        return 200;
+    }
+    catch(Exception $e) {
+        header("HTTP/1.1 500 Unexpeced error: " . $e->getMessage());
+        syslog(LOG_ERR, "[APIv3] HTTP/1.1 500 Unexpected error: " . $e->getMessage());
+        return 500;
+    }
+?>

+ 211 - 0
application/API/v3/profile/POST.php

@@ -0,0 +1,211 @@
+<?php
+    /**
+     * Profile creator script.
+     *
+     * Exposes an API to create a new profile.
+     *
+     * Mandatory query parameters are:
+     *  - id: Player ID.
+     *
+     * Mandatory POST parameters are:
+     *  - email: User email.
+     *  - password: User password.
+     *
+     * Optional POST parameters are:
+     *  - name: User name.
+     *  - public: 1 To make the profile public, 0 for private.
+     *
+     * @category API
+     */
+
+    global $db;
+
+    try{
+
+        header("Content-type: application/json; charset=utf-8");
+
+        // Get put data
+        parse_str(file_get_contents("php://input"), $_POST);
+
+        // Get profile ID
+        // $query heredated from API_CONTROLLER
+        if (count($query) > 0){
+            $id = $query[0];
+        }
+        else{
+            // ID is mandatory
+            header("HTTP/1.1 400 User ID not received.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 400 User ID not received.");
+            return 400;
+        }
+
+        // Check user mail.
+        $mail = $_POST["email"];
+        if ($mail == null || $mail == false){
+            header("HTTP/1.1 400 User email not received.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 400 User email not received.");
+            return 400;
+        }
+        if (!filter_var($mail, FILTER_VALIDATE_EMAIL)) {
+            header("HTTP/1.1 400 Invalid email.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 400 Invalid email.");
+            return 400;
+        }
+        // Check user password.
+        $password = $_POST["password"];
+        if ($password == null || $password == false){
+            header("HTTP/1.1 400 User password not received.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 400 User password not received.");
+            return 400;
+        }
+        // Check user name.
+        $name = $_POST["name"];
+        if ($name == null || $name == false){
+            $name = "player_" . $uid;
+        }
+        // Check user name.
+        $public = intval($_POST["public"]);
+        if ($public != 1){
+            $public = 0;
+        }
+
+        // Check if player exists
+        $statement = $db->prepare("SELECT COUNT(uid) AS c FROM player WHERE uid = :uid;");
+        $statement->bindValue(":uid", $uid);
+        if (1 != $statement->execute()->fetchArray(SQLITE3_ASSOC)["c"]){
+            header("HTTP/1.1 400 Existing user.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 400 Existing user.");
+            return 400;
+        }
+
+        // Prepare inputs
+        $password = hash("sha256", $password);
+        $characters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+        $charactersLength = 16;
+        $api_key = "";
+        for ($i = 0; $i < $length; $i++) {
+            $api_key .= $characters[rand(0, $charactersLength - 1)];
+        }
+
+        // Insert
+        $statement = $db->prepare("
+          INSERT INTO player (
+            uid,
+            name,
+            mail,
+            password,
+            api_key,
+            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_mark,
+            event_coin,
+            storage_slots,
+            island,
+            public
+          ) VALUES (
+            :uid,
+            :name,
+            :mail,
+            :password,
+            :api_key,
+            :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_mark,
+            :event_coin,
+            :storage_slots,
+            :island,
+            :public
+          );
+        ");
+        $statement->bindValue(":uid", $uid);
+        $statement->bindValue(":name", $name);
+        $statement->bindValue(":mail", $mail);
+        $statement->bindValue(":password", $password);
+        $statement->bindValue(":api_key", $api_key);
+        $statement->bindValue(":mana", 0);
+        $statement->bindValue(":crystal", 0);
+        $statement->bindValue(":country", null);
+        $statement->bindValue(":lang", null);
+        $statement->bindValue(":level", 1);
+        $statement->bindValue(":experience", 0);
+        $statement->bindValue(":energy", 0);
+        $statement->bindValue(":energy_max", 0);
+        $statement->bindValue(":energy_per_min", 0);
+        $statement->bindValue(":arena_energy", 0);
+        $statement->bindValue(":arena_energy_max", 10);
+        $statement->bindValue(":rep", null);
+        $statement->bindValue(":social_point", 0);
+        $statement->bindValue(":honor_point", 0);
+        $statement->bindValue(":guild_point", 0);
+        $statement->bindValue(":darkportal_energy", 0);
+        $statement->bindValue(":darkportal_energy_max", 10);
+        $statement->bindValue(":dimension_energy", 0);
+        $statement->bindValue(":dimension_energy_max", 100);
+        $statement->bindValue(":costume_point", 0);
+        $statement->bindValue(":costume_point_max", 0);
+        $statement->bindValue(":honor_medal", 0);
+        $statement->bindValue(":honor_mark", 0);
+        $statement->bindValue(":event_coin", 0);
+        $statement->bindValue(":storage_slots", 0);
+        $statement->bindValue(":island_upgrade", 0);
+        $statement->bindValue(":public", $public);
+        $statement->execute();
+
+        // Build array
+        $player = [
+            "username" => $name,
+            "email" => $mail,
+            "api_key" => $api_key,
+            "public" => "1"
+        ];
+        echo(json_encode($player));
+        header("HTTP/1.1 201 Created.");
+        syslog(LOG_INFO, "[APIv3] HTTP/1.1 201 Created.");
+        return 201;
+    }
+    catch(Exception $e) {
+        header("HTTP/1.1 500 Unexpeced error: " . $e->getMessage());
+        syslog(LOG_ERR, "[APIv3] HTTP/1.1 500 Unexpected error: " . $e->getMessage());
+        return 500;
+    }
+?>

+ 1196 - 0
application/API/v3/profile/PUT.php

@@ -0,0 +1,1196 @@
+<?php
+    /**
+     * Profile uploader script.
+     *
+     * Exposes an API to update an existing profile.
+     *
+     * It also saves the data to a JSON file in the data directory.
+     *
+     * Mandatory PUT parameters are:
+     *  - response: JSON received from game server on HubUserLogin command.
+     *  - key: User API key.
+     *
+     * @category API
+     */
+
+    global $db;
+
+    /**
+     * Calculates the currrent efficiency of a rune.
+     *
+     * @param JSON $rune JSON fragment of the rune.
+     * @return int Efficiency (0-100)
+     */
+    function calculate_efficiency($rune){
+        $efficiency = 0.0;
+        $max_efficiency = 0.0;
+        $substats = [];
+        $level = $rune->{"upgrade_curr"};
+        $stars = $rune->{"class"};
+        // Ancient stars are + 10
+        if ($stars > 10){
+            $stars -= 10;
+        }
+        $list = null;
+        // Main stat
+        $main_stat = $rune->{"pri_eff"}[0];
+        switch ($main_stat){
+            case RUNE_STAT_ID::HP:
+                $list = RUNE_STAT_VALUES::HP;
+                break;
+            case RUNE_STAT_ID::HP_P:
+                $list = RUNE_STAT_VALUES::HP_P;
+                break;
+            case RUNE_STAT_ID::ATK:
+                $list = RUNE_STAT_VALUES::ATK;
+                break;
+            case RUNE_STAT_ID::ATK_P:
+                $list = RUNE_STAT_VALUES::ATK_P;
+                break;
+            case RUNE_STAT_ID::DEF:
+                $list = RUNE_STAT_VALUES::DEF;
+                break;
+            case RUNE_STAT_ID::DEF_P:
+                $list = RUNE_STAT_VALUES::DEF_P;
+                break;
+            case RUNE_STAT_ID::SPD:
+                $list = RUNE_STAT_VALUES::SPD;
+                break;
+            case RUNE_STAT_ID::CRR:
+                $list = RUNE_STAT_VALUES::CRR;
+                break;
+            case RUNE_STAT_ID::CRD:
+                $list = RUNE_STAT_VALUES::CRD;
+                break;
+            case RUNE_STAT_ID::ACC:
+                $list = RUNE_STAT_VALUES::ACC;
+                break;
+            case RUNE_STAT_ID::RES:
+                $list = RUNE_STAT_VALUES::RES;
+                break;
+        }
+        $efficiency += floatval($list[$stars][$level]) / floatval($list[6][15]);
+        // Innate stat
+        if ($rune->{"prefix_eff"}[0] > 0){
+            $innate_stat = $rune->{"pri_eff"}[0];
+            $innate_stat_value = $rune->{"pri_eff"}[1];
+            $efficiency += floatval($innate_stat_value) / (5 * floatval(RUNE_SUBSTAT_MAX_VALUES::SUBSTAT[$innate_stat][6]));
+        }
+        // Substats
+        for ($i = 0; $i <= 3; $i ++){
+            if (sizeof($rune->{"sec_eff"}) > $i){
+                $substat = $rune->{"sec_eff"}[$i][0];
+                $substat_value = $rune->{"sec_eff"}[$i][1];
+                $efficiency += floatval($substat_value) / (5 * floatval(RUNE_SUBSTAT_MAX_VALUES::SUBSTAT[$substat][6]));
+            }
+        }
+        $efficiency = $efficiency / 2.8 * 100;
+        if ($efficiency > 100){
+            $efficiency = 100;
+        }
+        return $efficiency;
+    }
+
+    /**
+     * Calculates the maximum efficiency of a rune.
+     *
+     * @param JSON $rune JSON fragment of the rune.
+     * @return int Maximum efficiency (0-100)
+     */
+    function calculate_maximum_efficiency($rune){
+        $efficiency = 0.0;
+        $max_efficiency = 0.0;
+        $substats = [];
+        $stars = $rune->{"class"};
+        // Ancient stars are + 10
+        if ($stars > 10){
+            $stars -= 10;
+        }
+        $list = null;
+        // Main stat
+        $main_stat = $rune->{"pri_eff"}[0];
+        switch ($main_stat){
+            case RUNE_STAT_ID::HP:
+                $list = RUNE_STAT_VALUES::HP;
+                break;
+            case RUNE_STAT_ID::HP_P:
+                $list = RUNE_STAT_VALUES::HP_P;
+                break;
+            case RUNE_STAT_ID::ATK:
+                $list = RUNE_STAT_VALUES::ATK;
+                break;
+            case RUNE_STAT_ID::ATK_P:
+                $list = RUNE_STAT_VALUES::ATK_P;
+                break;
+            case RUNE_STAT_ID::DEF:
+                $list = RUNE_STAT_VALUES::DEF;
+                break;
+            case RUNE_STAT_ID::DEF_P:
+                $list = RUNE_STAT_VALUES::DEF_P;
+                break;
+            case RUNE_STAT_ID::SPD:
+                $list = RUNE_STAT_VALUES::SPD;
+                break;
+            case RUNE_STAT_ID::CRR:
+                $list = RUNE_STAT_VALUES::CRR;
+                break;
+            case RUNE_STAT_ID::CRD:
+                $list = RUNE_STAT_VALUES::CRD;
+                break;
+            case RUNE_STAT_ID::ACC:
+                $list = RUNE_STAT_VALUES::ACC;
+                break;
+            case RUNE_STAT_ID::RES:
+                $list = RUNE_STAT_VALUES::RES;
+                break;
+        }
+        $efficiency += floatval($list[$stars][15]) / floatval($list[6][15]);
+        // Innate stat
+        if ($rune->{"prefix_eff"}[0] > 0){
+            $innate_stat = $rune->{"pri_eff"}[0];
+            $innate_stat_value = $rune->{"pri_eff"}[1];
+            $efficiency += floatval($innate_stat_value) / (5 * floatval(RUNE_SUBSTAT_MAX_VALUES::SUBSTAT[$innate_stat][6]));
+        }
+        // Substats
+        for ($i = 0; $i <= 3; $i ++){
+            if (sizeof($rune->{"sec_eff"}) > $i){
+                $substat = $rune->{"sec_eff"}[$i][0];
+                $efficiency += floatval(RUNE_SUBSTAT_MAX_VALUES::SUBSTAT[$substat][$stars]) / (5 * floatval(RUNE_SUBSTAT_MAX_VALUES::SUBSTAT[$substat][6]));
+            }
+        }
+        $efficiency = $efficiency / 2.8 * 100;
+        if ($efficiency > 100){
+            $efficiency = 100;
+        }
+        return $efficiency;
+    }
+    
+    /**
+     * Parses an artifact and saves it to the database.
+     *
+     * @param string $uid Owner ID.
+     * @param JSON artifact JSON fragment of the artifact.
+     */
+    function parse_artifact($uid, $artifact){
+        global $db;
+        $statement = $db->prepare("
+          INSERT INTO artifact (
+            uid,
+            id,
+            assigned_to,
+            type,
+            attribute,
+            archetype,
+            level,
+            quality,
+            original_quality,
+            value,
+            efficiency,
+            max_efficiency,
+            main_stat,
+            main_stat_value,
+            effect_1,
+            effect_1_value,
+            effect_1_enchant,
+            effect_1_grind,
+            effect_2,
+            effect_2_value,
+            effect_2_enchant,
+            effect_2_grind,
+            effect_3,
+            effect_3_value,
+            effect_3_enchant,
+            effect_3_grind,
+            effect_4,
+            effect_4_value,
+            effect_4_enchant,
+            effect_4_grind,
+            locked
+          ) VALUES (
+            :uid,
+            :id,
+            :assigned_to,
+            :type,
+            :attribute,
+            :archetype,
+            :level,
+            :quality,
+            :original_quality,
+            :value,
+            :efficiency,
+            :max_efficiency,
+            :main_stat,
+            :main_stat_value,
+            :effect_1,
+            :effect_1_value,
+            :effect_1_enchant,
+            :effect_1_grind,
+            :effect_2,
+            :effect_2_value,
+            :effect_2_enchant,
+            :effect_2_grind,
+            :effect_3,
+            :effect_3_value,
+            :effect_3_enchant,
+            :effect_3_grind,
+            :effect_4,
+            :effect_4_value,
+            :effect_4_enchant,
+            :effect_4_grind,
+            :locked
+          );
+        ");
+        $statement->bindValue(":uid", $uid);
+        $statement->bindValue(":id", $artifact->{"rid"});
+        if (intval($artifact->{"occupied_id"}) == 0){
+            $statement->bindValue(":assigned_to", null);
+        }
+        else{
+            $statement->bindValue(":assigned_to", $artifact->{"occupied_id"});
+        }
+        if (intval($artifact->{"attribute"}) == 0){
+            $statement->bindValue(":attribute", null);
+        }
+        else{
+            $statement->bindValue(":attribute", $artifact->{"attribute"});
+        }
+        if (intval($artifact->{"type"}) == 0){
+            $statement->bindValue(":type", null);
+        }
+        else{
+            $statement->bindValue(":type", $artifact->{"type"});
+        }
+        if (intval($artifact->{"unit_style"}) == 0){
+            $statement->bindValue(":archetype", null);
+        }
+        else{
+            $statement->bindValue(":archetype", $artifact->{"unit_style"});
+        }
+        $statement->bindValue(":level", $artifact->{"level"});
+        $statement->bindValue(":quality", $artifact->{"rank"});
+        $statement->bindValue(":original_quality", $artifact->{"natural_rank"});
+        $statement->bindValue(":value", 0); // Not in JSON file
+        $statement->bindValue(":locked", $artifact->{"locked"});
+        $efficiency = 0; // Not standard formula yet
+        $max_efficiency = 0; // Not standard formula yet
+        $statement->bindValue(":efficiency", $efficiency);
+        $statement->bindValue(":max_efficiency", $max_efficiency);
+        $statement->bindValue(":main_stat", $artifact->{"pri_effect"}[0]);
+        $statement->bindValue(":main_stat_value", $artifact->{"pri_effect"}[1]);
+        if (sizeof($artifact->{"sec_effects"}) > 0){
+            $statement->bindValue(":effect_1", $artifact->{"sec_effects"}[0][0]);
+            $statement->bindValue(":effect_1_value", $artifact->{"sec_effects"}[0][1]);
+            $statement->bindValue(":effect_1_enchant", $artifact->{"sec_effects"}[0][2]);
+            $statement->bindValue(":effect_1_grind", $artifact->{"sec_effects"}[0][3]);
+        }
+        else{
+            $statement->bindValue(":effect_1", null);
+            $statement->bindValue(":effect_1_value", 0);
+            $statement->bindValue(":effect_1_enchant", 0);
+            $statement->bindValue(":effect_1_grind", 0);
+        }
+        if (sizeof($artifact->{"sec_effects"}) > 1){
+            $statement->bindValue(":effect_2", $artifact->{"sec_effects"}[1][0]);
+            $statement->bindValue(":effect_2_value", $artifact->{"sec_effects"}[1][1]);
+            $statement->bindValue(":effect_2_enchant", $artifact->{"sec_effects"}[1][2]);
+            $statement->bindValue(":effect_2_grind", $artifact->{"sec_effects"}[1][3]);
+        }
+        else{
+            $statement->bindValue(":effect_2", null);
+            $statement->bindValue(":effect_2_value", 0);
+            $statement->bindValue(":effect_2_enchant", 0);
+            $statement->bindValue(":effect_2_grind", 0);
+        }
+        if (sizeof($artifact->{"sec_effects"}) > 2){
+            $statement->bindValue(":effect_3", $artifact->{"sec_effects"}[2][0]);
+            $statement->bindValue(":effect_3_value", $artifact->{"sec_effects"}[2][1]);
+            $statement->bindValue(":effect_3_enchant", $artifact->{"sec_effects"}[2][2]);
+            $statement->bindValue(":effect_3_grind", $artifact->{"sec_effects"}[2][3]);
+        }
+        else{
+            $statement->bindValue(":effect_3", null);
+            $statement->bindValue(":effect_3_value", 0);
+            $statement->bindValue(":effect_3_enchant", 0);
+            $statement->bindValue(":effect_3_grind", 0);
+        }
+        if (sizeof($artifact->{"sec_effects"}) > 3){
+            $statement->bindValue(":effect_4", $artifact->{"sec_effects"}[3][0]);
+            $statement->bindValue(":effect_4_value", $artifact->{"sec_effects"}[3][1]);
+            $statement->bindValue(":effect_4_enchant", $artifact->{"sec_effects"}[3][2]);
+            $statement->bindValue(":effect_4_grind", $artifact->{"sec_effects"}[3][3]);
+        }
+        else{
+            $statement->bindValue(":effect_4", null);
+            $statement->bindValue(":effect_4_value", 0);
+            $statement->bindValue(":effect_4_enchant", 0);
+            $statement->bindValue(":effect_4_grind", 0);
+        }
+        $statement->execute();
+    }
+
+    /**
+     * Parses a rune and saves it to the database.
+     *
+     * @param string $uid Owner ID.
+     * @param JSON rune JSON fragment of the rune.
+     */
+    function parse_rune($uid, $rune, $lock_list){
+        global $db;
+        $statement = $db->prepare("
+          INSERT INTO rune (
+            uid,
+            id,
+            assigned_to,
+            type,
+            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,
+            locked
+          ) VALUES (
+            :uid,
+            :id,
+            :assigned_to,
+            :type,
+            :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,
+            :locked
+          );
+        ");
+        $statement->bindValue(":uid", $uid);
+        $statement->bindValue(":id", $rune->{"rune_id"});
+        if (intval($rune->{"occupied_id"}) == 0){
+            $statement->bindValue(":assigned_to", null);
+        }
+        else{
+            $statement->bindValue(":assigned_to", $rune->{"occupied_id"});
+        }
+        $statement->bindValue(":type", $rune->{"set_id"});
+        $statement->bindValue(":slot", $rune->{"slot_no"});
+        $statement->bindValue(":value", $rune->{"sell_value"});
+        if ($rune->{"class"} > 10){
+            // Ancient rune
+            $statement->bindValue(":ancient", 1);
+            $statement->bindValue(":stars", intval($rune->{"class"}) - 10);
+            $statement->bindValue(":original_quality", intval($rune->{"extra"}) - 10);
+        }
+        else{
+            // Non Ancient rune
+            $statement->bindValue(":ancient", 0);
+            $statement->bindValue(":stars", $rune->{"class"});
+            $statement->bindValue(":original_quality", $rune->{"extra"});
+        }
+        $level = $rune->{"upgrade_curr"};
+        $statement->bindValue(":level", $level);
+        $quality = QUALITY_ID::NORMAL;
+        if ($level >= 12){
+            $quality = QUALITY_ID::LEGEND;
+        }
+        elseif ($level >= 9){
+            $quality = QUALITY_ID::HERO;
+        }
+        elseif ($level >= 6){
+            $quality = QUALITY_ID::RARE;
+        }
+        elseif ($level >= 3){
+            $quality = QUALITY_ID::MAGIC;
+        }
+        if ($rune->{"extra"} > $quality){
+            $quality = $rune->{"extra"};
+        }
+        $statement->bindValue(":quality", $quality);
+        $lock = 0;
+        if (in_array($rune->{"rune_id"}, $lock_list)){
+            $lock = 1;
+        }
+        $statement->bindValue(":locked", $lock);
+        $efficiency = calculate_efficiency($rune);
+        $max_efficiency = calculate_maximum_efficiency($rune);
+        $statement->bindValue(":efficiency", $efficiency);
+        $statement->bindValue(":max_efficiency", $max_efficiency);
+        $statement->bindValue(":main_stat", $rune->{"pri_eff"}[0]);
+        $statement->bindValue(":main_stat_value", $rune->{"pri_eff"}[1]);
+        if ($rune->{"prefix_eff"}[0] > 0){
+            $statement->bindValue(":innate_stat", $rune->{"prefix_eff"}[0]);
+            $statement->bindValue(":innate_stat_value", $rune->{"prefix_eff"}[1]);
+        }
+        else{
+            $statement->bindValue(":innate_stat", null);
+            $statement->bindValue(":innate_stat_value", 0);
+        }
+        if (sizeof($rune->{"sec_eff"}) > 0){
+            $statement->bindValue(":substat_1", $rune->{"sec_eff"}[0][0]);
+            $statement->bindValue(":substat_1_value", $rune->{"sec_eff"}[0][1]);
+            $statement->bindValue(":substat_1_enchant", $rune->{"sec_eff"}[0][2]);
+            $statement->bindValue(":substat_1_grind", $rune->{"sec_eff"}[0][3]);
+        }
+        else{
+            $statement->bindValue(":substat_1", null);
+            $statement->bindValue(":substat_1_value", 0);
+            $statement->bindValue(":substat_1_enchant", 0);
+            $statement->bindValue(":substat_1_grind", 0);
+        }
+        if (sizeof($rune->{"sec_eff"}) > 1){
+            $statement->bindValue(":substat_2", $rune->{"sec_eff"}[1][0]);
+            $statement->bindValue(":substat_2_value", $rune->{"sec_eff"}[1][1]);
+            $statement->bindValue(":substat_2_enchant", $rune->{"sec_eff"}[1][2]);
+            $statement->bindValue(":substat_2_grind", $rune->{"sec_eff"}[1][3]);
+        }
+        else{
+            $statement->bindValue(":substat_2", null);
+            $statement->bindValue(":substat_2_value", 0);
+            $statement->bindValue(":substat_2_enchant", 0);
+            $statement->bindValue(":substat_2_grind", 0);
+        }
+        if (sizeof($rune->{"sec_eff"}) > 2){
+            $statement->bindValue(":substat_3", $rune->{"sec_eff"}[2][0]);
+            $statement->bindValue(":substat_3_value", $rune->{"sec_eff"}[2][1]);
+            $statement->bindValue(":substat_3_enchant", $rune->{"sec_eff"}[2][2]);
+            $statement->bindValue(":substat_3_grind", $rune->{"sec_eff"}[2][3]);
+        }
+        else{
+            $statement->bindValue(":substat_3", null);
+            $statement->bindValue(":substat_3_value", 0);
+            $statement->bindValue(":substat_3_enchant", 0);
+            $statement->bindValue(":substat_3_grind", 0);
+        }
+        if (sizeof($rune->{"sec_eff"}) > 3){
+            $statement->bindValue(":substat_4", $rune->{"sec_eff"}[3][0]);
+            $statement->bindValue(":substat_4_value", $rune->{"sec_eff"}[3][1]);
+            $statement->bindValue(":substat_4_enchant", $rune->{"sec_eff"}[3][2]);
+            $statement->bindValue(":substat_4_grind", $rune->{"sec_eff"}[3][3]);
+        }
+        else{
+            $statement->bindValue(":substat_4", null);
+            $statement->bindValue(":substat_4_value", 0);
+            $statement->bindValue(":substat_4_enchant", 0);
+            $statement->bindValue(":substat_4_grind", 0);
+        }
+        $statement->execute();
+    }
+
+    try{
+
+        header("Content-type: application/json; charset=utf-8");
+
+        // Get put data
+        parse_str(file_get_contents("php://input"), $_PUT);
+
+        // Check API key.
+        $key = $_PUT["key"];
+        if ($key == null || $key == false){
+            header("HTTP/1.1 400 API key not received.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 400 API key not received.");
+            return 400;
+        }
+
+        // Check data
+        $data = $_PUT["response"];
+        if ($data == null || $data == false){
+            header("HTTP/1.1 400 No data received.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 400 No data received.");
+            return 400;
+        }
+
+        // Check data format.
+        $json = json_decode($data);
+        if ($json === null){
+            header("HTTP/1.1 400 Data is no valid JSON.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 400 Data is no valid JSON.");
+            return 400;
+        }
+
+        // Authenticate
+        $uid = $json->{"wizard_id"};
+        $uname = $json->{"wizard_info"}->{"wizard_name"};
+        $statement = $db->prepare("SELECT COUNT(uid) AS c FROM player WHERE uid = :uid AND api_key = :api_key';");
+        $statement->bindValue(":uid", $uid);
+        $statement->bindValue(":api_key", $key);
+        if (1 != $statement->execute()->fetchArray(SQLITE3_ASSOC)["c"]){
+            header("HTTP/1.1 401 Invalid credentials.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 401 Invalid credentials.");
+            return 401;
+        }
+
+        // Save 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) {
+            header("HTTP/1.1 500 Unable to save profile file: " . $e->getMessage());
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 500 Unable to save profile file: " . $e->getMessage());
+            return 500;
+        }
+
+        // Clear previous profile data
+        $db->query("PRAGMA foreign_keys = OFF;");
+        $statement = $db->prepare("DELETE FROM unit_skill WHERE unit IN (SELECT id FROM unit WHERE uid = :uid);");
+        $statement->bindValue(":uid", $uid);
+        $statement->execute();
+        $tables_to_clear = [
+            "scenario",
+            "defense",
+            "gw_defense",
+            "unit",
+            "rune",
+            "artifact",
+            "building",
+            "decoration",
+            "inventory",
+            "rune_craft"
+        ];
+        foreach ($tables_to_clear as $table){
+            $statement = $db->prepare("DELETE FROM $table WHERE uid = :uid;");
+            $statement->bindValue(":uid", $uid);
+            $statement->execute();
+        }
+        $db->query("DELETE FROM summon_special"); # For every player, will be reloaded.
+
+        // Get rune lock list
+        $rune_lock_list = $json->{"rune_lock_list"};
+
+        // Update player data
+        $statement = $db->prepare("
+          UPDATE player
+          SET
+            name = :name AND
+            mana = :mana AND
+            crystal = :crystal AND
+            country = :country AND
+            lang = :lang AND
+            level = :level AND
+            experience = :experience AND
+            energy = :energy AND
+            energy_max = :energy_max AND
+            energy_per_min = :energy_per_min AND
+            arena_energy = :arena_energy AND
+            arena_energy_max = :arena_energy_max AND
+            rep = :rep AND
+            social_point = :social_point AND
+            honor_point = :honor_point AND
+            guild_point = :guild_point AND
+            darkportal_energy = :darkportal_energy AND
+            darkportal_energy_max = :darkportal_energy_max AND
+            dimension_energy = :dimension_energy AND
+            dimension_energy_max = :dimension_energy_max AND
+            costume_point = :costume_point AND
+            costume_point_max = :costume_point_max AND
+            honor_medal = :honor_medal AND
+            honor_mark = :honor_mark AND
+            event_coin = :event_coin AND
+            storage_slots = :storage_slots AND
+            island = :island_upgrade
+          WHERE uid = :uid;
+        ");
+        $statement->bindValue(":name", $json->{"wizard_info"}->{"wizard_name"});
+        $statement->bindValue(":mana", $json->{"wizard_info"}->{"wizard_mana"});
+        $statement->bindValue(":crystal", $json->{"wizard_info"}->{"wizard_crystal"});
+        $statement->bindValue(":country", $json->{"wizard_info"}->{"wizard_last_country"});
+        $statement->bindValue(":lang", $json->{"wizard_info"}->{"wizard_last_lang"});
+        $statement->bindValue(":level", $json->{"wizard_info"}->{"wizard_level"});
+        $statement->bindValue(":experience", $json->{"wizard_info"}->{"experience"});
+        $statement->bindValue(":energy", $json->{"wizard_info"}->{"wizard_energy"});
+        $statement->bindValue(":energy_max", $json->{"wizard_info"}->{"energy_max"});
+        $statement->bindValue(":energy_per_min", $json->{"wizard_info"}->{"energy_per_min"});
+        $statement->bindValue(":arena_energy", $json->{"wizard_info"}->{"arena_energy"});
+        $statement->bindValue(":arena_energy_max", $json->{"wizard_info"}->{"arena_energy_max"});
+        $statement->bindValue(":rep", $json->{"wizard_info"}->{"rep_unit_id"});
+        $statement->bindValue(":social_point", $json->{"wizard_info"}->{"social_point_current"});
+        $statement->bindValue(":honor_point", $json->{"wizard_info"}->{"honor_point"});
+        $statement->bindValue(":guild_point", $json->{"wizard_info"}->{"guild_point"});
+        $statement->bindValue(":darkportal_energy", $json->{"wizard_info"}->{"darkportal_energy"});
+        $statement->bindValue(":darkportal_energy_max", $json->{"wizard_info"}->{"darkportal_energy_max"});
+        $statement->bindValue(":dimension_energy", $json->{"dimension_hole_info"}->{"energy"});
+        $statement->bindValue(":dimension_energy_max", $json->{"dimension_hole_info"}->{"energy_max"});
+        $statement->bindValue(":costume_point", $json->{"wizard_info"}->{"costume_point"});
+        $statement->bindValue(":costume_point_max", $json->{"wizard_info"}->{"costume_point_max"});
+        $statement->bindValue(":honor_medal", $json->{"wizard_info"}->{"honor_medal"});
+        $statement->bindValue(":honor_mark", $json->{"wizard_info"}->{"honor_mark"});
+        $statement->bindValue(":event_coin", $json->{"wizard_info"}->{"event_coin"});
+        $statement->bindValue(":storage_slots", $json->{"unit_depository_slots"}->{"number"});
+        $island_upgrade = 0;
+        foreach ($json->{"island_info"} as $island){
+            if (intval($island->{"id"}) > 100 && intval($island->{"open"}) == 1){
+                $island_upgrade = intval($island->{"id"});
+            }
+        }
+        $statement->bindValue(":island_upgrade", $island_upgrade);
+        $statement->bindValue(":uid", $uid);
+        $statement->execute();
+
+        // Update scenarion completion
+        foreach ($json->{"scenario_list"} as $sce){
+            $statement = $db->prepare("
+              INSERT INTO scenario (
+                uid,
+                region,
+                difficulty,
+                cleared,
+                max_cleared
+              ) VALUES (
+                :uid,
+                :region,
+                :difficulty,
+                :cleared,
+                :max_cleared
+              );
+            ");
+            $statement->bindValue(":uid", $uid);
+            $statement->bindValue(":region", $sce->{"region_id"});
+            $statement->bindValue(":difficulty", $sce->{"difficulty"});
+            $statement->bindValue(":cleared", $sce->{"cleared"});
+            $max_cleared = 0;
+            foreach ($sce->{"stage_list"} as $stage){
+                if (intval($stage->{"cleared"}) == 1){
+                    $max_cleared = intval($stage->{"stage_no"});
+                }
+            }
+            $statement->bindValue(":max_cleared", $max_cleared);
+            $statement->execute();
+        }
+
+        // Parse Arena defense
+        foreach ($json->{"defense_unit_list"} as $defense){
+            $statement = $db->prepare("
+              INSERT INTO defense (
+                uid,
+                unit,
+                position
+              ) VALUES (
+                :uid,
+                :unit,
+                :position
+              );
+            ");
+            $statement->bindValue(":uid", $uid);
+            $statement->bindValue(":unit", $defense->{"unit_id"});
+            $statement->bindValue(":position", $defense->{"pos_id"});
+            $statement->execute();
+        }
+
+        // Parse GW defense
+        if ($json->{"guildwar_defense_unit_list"}[0]){
+            $position = 1;
+            foreach ($json->{"guildwar_defense_unit_list"}[0] as $defense){
+                $statement = $db->prepare("
+                  INSERT INTO gw_defense (
+                    uid,
+                    unit,
+                    position
+                  ) VALUES (
+                    :uid,
+                    :unit,
+                    :position
+                  );
+                ");
+                $statement->bindValue(":uid", $uid);
+                $statement->bindValue(":unit", $defense->{"unit_id"});
+                $statement->bindValue(":position", $position);
+                $statement->execute();
+                $position ++;
+            }
+        }
+        if ($json->{"guildwar_defense_unit_list"}[1]){
+            $position = 4;
+            foreach ($json->{"guildwar_defense_unit_list"}[1] as $defense){
+                $statement = $db->prepare("
+                  INSERT INTO gw_defense (
+                    uid,
+                    unit,
+                    position
+                  ) VALUES (
+                    :uid,
+                    :unit,
+                    :position
+                  );
+                ");
+                $statement->bindValue(":uid", $uid);
+                $statement->bindValue(":unit", $defense->{"unit_id"});
+                $statement->bindValue(":position", $position);
+                $statement->execute();
+                $position ++;
+            }
+        }
+
+        // Parse buildings
+        foreach($json->{"building_list"} as $building){
+            $statement = $db->prepare("
+              INSERT INTO building (
+                uid,
+                id,
+                building,
+                gain
+              ) VALUES (
+                :uid,
+                :id,
+                :building,
+                :gain
+              );
+            ");
+            $statement->bindValue(":uid", $uid);
+            $statement->bindValue(":id", $building->{"building_id"});
+            $statement->bindValue(":building", $building->{"building_master_id"});
+            $statement->bindValue(":gain", $building->{"gain_per_hour"});
+            $statement->execute();
+        }
+
+        // Parse decorarion
+        foreach($json->{"deco_list"} as $building){
+            $statement = $db->prepare("
+              INSERT INTO decoration (
+                uid,
+                id,
+                decoration,
+                level
+              ) VALUES (
+                :uid,
+                :id,
+                :decoration,
+                :level
+              );
+            ");
+            $statement->bindValue(":uid", $uid);
+            $statement->bindValue(":id", $building->{"deco_id"});
+            $statement->bindValue(":decoration", $building->{"master_id"});
+            $statement->bindValue(":level", $building->{"level"});
+            $statement->execute();
+        }
+
+        // Parse units
+        $lock_list = $json->{"unit_lock_list"};
+        foreach ($json->{"unit_list"} as $unit){
+            $statement = $db->prepare("
+              INSERT INTO unit (
+                uid,
+                id,
+                building,
+                unit,
+                level,
+                stars,
+                hp,
+                attack,
+                defense,
+                speed,
+                crit_rate,
+                crit_damage,
+                resistance,
+                accuracy,
+                experience,
+                exp_gained,
+                exp_gain_rate,
+                costume,
+                attribute,
+                source,
+                create_time,
+                homunculus,
+                homunculus_name,
+                lock
+              ) VALUES (
+                :uid,
+                :id,
+                :building,
+                :unit,
+                :level,
+                :stars,
+                :hp,
+                :attack,
+                :defense,
+                :speed,
+                :crit_rate,
+                :crit_damage,
+                :resistance,
+                :accuracy,
+                :experience,
+                :exp_gained,
+                :exp_gain_rate,
+                :costume,
+                :attribute,
+                :source,
+                :create_time,
+                :homunculus,
+                :homunculus_name,
+                :lock
+              );
+            ");
+            $statement->bindValue(":uid", $uid);
+            $statement->bindValue(":id", $unit->{"unit_id"});
+            $statement->bindValue(":building", $unit->{"building_id"});
+            $statement->bindValue(":unit", $unit->{"unit_master_id"});
+            $statement->bindValue(":level", $unit->{"unit_level"});
+            $statement->bindValue(":stars", $unit->{"class"});
+            $statement->bindValue(":hp", intval($unit->{"con"}) * 15); # Always x15
+            $statement->bindValue(":attack", $unit->{"atk"});
+            $statement->bindValue(":defense", $unit->{"def"});
+            $statement->bindValue(":speed", $unit->{"spd"});
+            $statement->bindValue(":crit_rate", $unit->{"critical_rate"});
+            $statement->bindValue(":crit_damage", $unit->{"critical_damage"});
+            $statement->bindValue(":resistance", $unit->{"resist"});
+            $statement->bindValue(":accuracy", $unit->{"accuracy"});
+            $statement->bindValue(":experience", $unit->{"experience"});
+            $statement->bindValue(":exp_gained", $unit->{"exp_gained"});
+            $statement->bindValue(":exp_gain_rate", $unit->{"exp_gain_rate"});
+            $statement->bindValue(":costume", $unit->{"costume_master_id"});
+            $statement->bindValue(":attribute", $unit->{"attribute"});
+            $statement->bindValue(":source", $unit->{"source"});
+            $statement->bindValue(":create_time", $unit->{"create_time"});
+            $statement->bindValue(":homunculus", $unit->{"homunculus"});
+            $statement->bindValue(":homunculus_name", $unit->{"homunculus_name"});
+            $lock = 0;
+            if (in_array($unit->{"unit_id"}, $lock_list)){
+                $lock = 1;
+            }
+            $statement->bindValue(":lock", $lock);
+            $statement->execute();
+            foreach ($unit->{"skills"} as $skill){
+                $statement = $db->prepare("
+                  INSERT INTO unit_skill (
+                    unit,
+                    skill,
+                    level
+                  ) VALUES (
+                    :unit,
+                    :skill,
+                    :level
+                  );
+                ");
+                $statement->bindValue(":unit", $unit->{"unit_id"});
+                $statement->bindValue(":skill", $skill[0]);
+                $statement->bindValue(":level", $skill[1]);
+                $statement->execute();
+            }
+        }
+
+        // Parse invetory
+        foreach ($json->{"inventory_info"} as $item){
+            $statement = $db->prepare("
+              INSERT INTO inventory (
+                uid,
+                id,
+                type,
+                amount
+              ) VALUES (
+                :uid,
+                :id,
+                :type,
+                :amount
+              );
+            ");
+            $statement->bindValue(":uid", $uid);
+            $statement->bindValue(":id", $item->{"item_master_id"});
+            $statement->bindValue(":type", $item->{"item_master_type"});
+            $statement->bindValue(":amount", $item->{"item_quantity"});
+            $statement->execute();
+        }
+        // Fix rune craft item type.
+        $db->query("UPDATE inventory SET type = 27 WHERE type = 29 AND id IN (2001, 4001, 4002, 4003, 9001, 9002, 9003, 8001);");
+
+        // Parse Summon Stone summoning list
+        // TODO: Set propper dates
+        foreach ($json->{"summon_special_info"}->{"this"} as $unit){
+            $statement = $db->prepare("
+              INSERT INTO summon_special (
+                week,
+                unit
+              ) VALUES (
+                :week,
+                :unit
+              );
+            ");
+            $statement->bindValue(":week", 0);
+            $statement->bindValue(":unit", $unit);
+            $statement->execute();
+        }
+        foreach ($json->{"summon_special_info"}->{"next"} as $unit){
+            $statement = $db->prepare("
+              INSERT INTO summon_special (
+                week,
+                unit
+              ) VALUES (
+                :week,
+                :unit
+              );
+            ");
+            $statement->bindValue(":week", 1);
+            $statement->bindValue(":unit", $unit);
+            $statement->execute();
+        }
+        foreach ($json->{"summon_special_info"}->{"third"} as $unit){
+            $statement = $db->prepare("
+              INSERT INTO summon_special (
+                week,
+                unit
+              ) VALUES (
+                :week,
+                :unit
+              );
+            ");
+            $statement->bindValue(":week", 2);
+            $statement->bindValue(":unit", $unit);
+            $statement->execute();
+        }
+        foreach ($json->{"summon_special_info"}->{"fourth"} as $unit){
+            $statement = $db->prepare("
+              INSERT INTO summon_special (
+                week,
+                unit
+              ) VALUES (
+                :week,
+                :unit
+              );
+            ");
+            $statement->bindValue(":week", 3);
+            $statement->bindValue(":unit", $unit);
+            $statement->execute();
+        }
+
+        // Parse runes
+        foreach ($json->{"runes"} as $rune){
+            parse_rune($uid, $rune, $rune_lock_list);
+        }
+        foreach ($json->{"unit_list"} as $unit){
+            foreach ($unit->{"runes"} as $rune){
+                parse_rune($uid, $rune, $rune_lock_list);
+            }
+        }
+
+        // Parse artifacts
+        foreach ($json->{"artifacts"} as $artifact){
+            parse_artifact($uid, $artifact);
+        }
+        foreach ($json->{"unit_list"} as $unit){
+            foreach ($unit->{"artifacts"} as $artifact){
+                parse_artifact($uid, $artifact);
+            }
+        }
+
+        // Parse enchantments
+        foreach ($json->{"rune_craft_item_list"} as $item){
+            $statement = $db->prepare("
+              INSERT INTO rune_craft (
+                uid,
+                id,
+                type,
+                quality,
+                rune,
+                stat,
+                value,
+                amount
+              ) VALUES (
+                :uid,
+                :id,
+                :type,
+                :quality,
+                :rune,
+                :stat,
+                :value,
+                :amount
+              );
+            ");
+            $statement->bindValue(":uid", $uid);
+            $statement->bindValue(":id", $item->{"craft_item_id"});
+            $statement->bindValue(":type", $item->{"craft_type"});
+            $statement->bindValue(":value", $item->{"sell_value"});
+            $statement->bindValue(":amount", $item->{"amount"});
+            $info = str_pad(intval($item->{"craft_type_id"}), 6, "0", STR_PAD_LEFT);
+            /*
+            info : 5 or 6 digit number: RRSSQQ
+             RR: Rune (may not be padded)
+             SS: Stat
+             QQ:  Quality
+            */
+            $statement->bindValue(":quality", intval(substr($info, 4, 2)));
+            $statement->bindValue(":stat", intval(substr($info, 2, 2)));
+            $statement->bindValue(":rune", intval(substr($info, 0, 2)));
+            $statement->execute();
+        }
+
+        // Parse guild
+        if ($json->{"guild"}->{"guild_info"} != null){
+            $guild = $json->{"guild"}->{"guild_info"};
+            $statement = $db->prepare("DELETE FROM guild WHERE id = :id;");
+            $statement->bindValue(":id", $guild->{"guild_id"});
+            $statement->execute();
+            $statement = $db->prepare("DELETE FROM guild_member WHERE guild = :guild;");
+            $statement->bindValue(":guild", $guild->{"guild_id"});
+            $statement->execute();
+            $statement = $db->prepare("
+              INSERT INTO guild (
+                id,
+                name,
+                level,
+                experience,
+                recruiting,
+                members,
+                leader,
+                comment,
+                notice
+              ) VALUES (
+                :id,
+                :name,
+                :level,
+                :experience,
+                :recruiting,
+                :members,
+                :leader,
+                :comment,
+                :notice
+              );
+            ");
+            $statement->bindValue(":id",$guild->{"guild_id"});
+            $statement->bindValue(":name",$guild->{"name"});
+            $statement->bindValue(":level",$guild->{"level"});
+            $statement->bindValue(":experience",$guild->{"experience"});
+            $statement->bindValue(":recruiting",$guild->{"recruit_status"});
+            $statement->bindValue(":members",$guild->{"member_now"});
+            $statement->bindValue(":leader",$guild->{"master_wizard_id"});
+            $statement->bindValue(":comment",$guild->{"comment"});
+            $statement->bindValue(":notice",$guild->{"notice"});
+            $statement->execute();
+            foreach ($json->{"guild"}->{"guild_members"} as $member){
+                $statement = $db->prepare("
+                  INSERT INTO guild_member (
+                    id,
+                    guild,
+                    grade,
+                    name,
+                    level,
+                    rating,
+                    arena_score,
+                    joined,
+                    last_login,
+                    in_war,
+                    has_defense
+                  ) VALUES (
+                    :id,
+                    :guild,
+                    :grade,
+                    :name,
+                    :level,
+                    :rating,
+                    :arena_score,
+                    :joined,
+                    :last_login,
+                    :in_war,
+                    :has_defense
+                  );
+                ");
+                $statement->bindValue(":id", $member->{"wizard_id"});
+                $statement->bindValue(":guild", $member->{"guild_id"});
+                $statement->bindValue(":grade", $member->{"grade"});
+                $statement->bindValue(":name", $member->{"wizard_name"});
+                $statement->bindValue(":level", $member->{"wizard_level"});
+                $statement->bindValue(":rating", $member->{"rating_id"});
+                $statement->bindValue(":arena_score", $member->{"arena_score"});
+                $statement->bindValue(":joined", $member->{"join_timestamp"});
+                $statement->bindValue(":last_login", $member->{"last_login_timestamp"});
+                $in_war = 0;
+                foreach ($json->{"guildwar_member_list"} as $war){
+                    if ($war->{"wizard_id"} == $member->{"wizard_id"}){
+                        $in_war = 1;
+                        break;
+                    }
+                }
+                $has_defense = 0;
+                foreach ($json->{"guild_member_defense_list"} as $defense){
+                    if ($defense->{"wizard_id"} == $member->{"wizard_id"}){
+                        $has_defense = 1;
+                        break;
+                    }
+                }
+                $statement->bindValue(":in_war", $in_war);
+                $statement->bindValue(":has_defense", $has_defense);
+                $statement->execute();
+            }
+        }
+
+        // Update invalid teams
+        $statement = $db->prepare("
+          DELETE FROM team 
+          WHERE 
+            id IN (
+              SELECT DISTINCT team 
+              FROM team_unit 
+              WHERE unit NOT IN (
+                SELECT DISTINCT id FROM unit WHERE uid = :uid
+              )
+            ) AND
+            uid = :uid
+          ");
+        $statement->bindValue(":uid", $uid);
+        $statement->execute();
+        $db->query("
+          DELETE FROM team_unit
+          WHERE 
+            unit NOT IN (
+              SELECT DISTINCT id FROM unit
+            ) OR 
+            team NOT IN (
+              SELECT DISTINCT id FROM team
+            );"
+        );
+
+        // At this point, status code should be 200
+        header("HTTP/1.1 204 Profile imported.");
+        syslog(LOG_INFO, "[APIv3] HTTP/1.1 204 Profile imported.");
+        return 204;
+    }
+    catch(Exception $e) {
+        header("HTTP/1.1 500 Unexpeced error: " . $e->getMessage());
+        syslog(LOG_INFO, "[APIv3] HTTP/1.1 500 Unexpected error: " . $e->getMessage());
+        return 500;
+    }
+?>

+ 556 - 0
application/API/v3/run/POST.php

@@ -0,0 +1,556 @@
+<?php
+    /**
+     * Run logger script.
+     *
+     * Exposes an API to log a run.
+     *
+     * Mandatory PUT parameters are:
+     *  - result_response: JSON received from game server on run end.
+     *  - key: User API key.
+     *
+     * Optional POST parameters are:
+     *  - result_request: JSON sent to game server on run end.
+     *  - start_response: JSON received from game server on run start.
+     *  - start_request: JSON sent to game server on run start.
+     *
+     * @category API
+     */
+
+    /**
+     * Calculates the currrent efficiency of a rune.
+     *
+     * @param JSON $rune JSON fragment of the rune.
+     * @return int Efficiency (0-100)
+     */
+    function calculate_efficiency($rune){
+        $efficiency = 0.0;
+        $max_efficiency = 0.0;
+        $substats = [];
+        $level = $rune->{"upgrade_curr"};
+        $stars = $rune->{"class"};
+        // Ancient stars are + 10
+        if ($stars > 10){
+            $stars -= 10;
+        }
+        $list = null;
+        // Main stat
+        $main_stat = $rune->{"pri_eff"}[0];
+        switch ($main_stat){
+            case RUNE_STAT_ID::HP:
+                $list = RUNE_STAT_VALUES::HP;
+                break;
+            case RUNE_STAT_ID::HP_P:
+                $list = RUNE_STAT_VALUES::HP_P;
+                break;
+            case RUNE_STAT_ID::ATK:
+                $list = RUNE_STAT_VALUES::ATK;
+                break;
+            case RUNE_STAT_ID::ATK_P:
+                $list = RUNE_STAT_VALUES::ATK_P;
+                break;
+            case RUNE_STAT_ID::DEF:
+                $list = RUNE_STAT_VALUES::DEF;
+                break;
+            case RUNE_STAT_ID::DEF_P:
+                $list = RUNE_STAT_VALUES::DEF_P;
+                break;
+            case RUNE_STAT_ID::SPD:
+                $list = RUNE_STAT_VALUES::SPD;
+                break;
+            case RUNE_STAT_ID::CRR:
+                $list = RUNE_STAT_VALUES::CRR;
+                break;
+            case RUNE_STAT_ID::CRD:
+                $list = RUNE_STAT_VALUES::CRD;
+                break;
+            case RUNE_STAT_ID::ACC:
+                $list = RUNE_STAT_VALUES::ACC;
+                break;
+            case RUNE_STAT_ID::RES:
+                $list = RUNE_STAT_VALUES::RES;
+                break;
+        }
+        $efficiency += floatval($list[$stars][$level]) / floatval($list[6][15]);
+        // Innate stat
+        if ($rune->{"prefix_eff"}[0] > 0){
+            $innate_stat = $rune->{"pri_eff"}[0];
+            $innate_stat_value = $rune->{"pri_eff"}[1];
+            $efficiency += floatval($innate_stat_value) / (5 * floatval(RUNE_SUBSTAT_MAX_VALUES::SUBSTAT[$innate_stat][6]));
+        }
+        // Substats
+        for ($i = 0; $i <= 3; $i ++){
+            if (sizeof($rune->{"sec_eff"}) > $i){
+                $substat = $rune->{"sec_eff"}[$i][0];
+                $substat_value = $rune->{"sec_eff"}[$i][1];
+                $efficiency += floatval($substat_value) / (5 * floatval(RUNE_SUBSTAT_MAX_VALUES::SUBSTAT[$substat][6]));
+            }
+        }
+        $efficiency = $efficiency / 2.8 * 100;
+        if ($efficiency > 100){
+            $efficiency = 100;
+        }
+        return $efficiency;
+    }
+
+    /**
+     * Calculates the maximum efficiency of a rune.
+     *
+     * @param JSON $rune JSON fragment of the rune.
+     * @return int Maximum efficiency (0-100)
+     */
+    function calculate_maximum_efficiency($rune){
+        $efficiency = 0.0;
+        $max_efficiency = 0.0;
+        $substats = [];
+        $stars = $rune->{"class"};
+        // Ancient stars are + 10
+        if ($stars > 10){
+            $stars -= 10;
+        }
+        $list = null;
+        // Main stat
+        $main_stat = $rune->{"pri_eff"}[0];
+        switch ($main_stat){
+            case RUNE_STAT_ID::HP:
+                $list = RUNE_STAT_VALUES::HP;
+                break;
+            case RUNE_STAT_ID::HP_P:
+                $list = RUNE_STAT_VALUES::HP_P;
+                break;
+            case RUNE_STAT_ID::ATK:
+                $list = RUNE_STAT_VALUES::ATK;
+                break;
+            case RUNE_STAT_ID::ATK_P:
+                $list = RUNE_STAT_VALUES::ATK_P;
+                break;
+            case RUNE_STAT_ID::DEF:
+                $list = RUNE_STAT_VALUES::DEF;
+                break;
+            case RUNE_STAT_ID::DEF_P:
+                $list = RUNE_STAT_VALUES::DEF_P;
+                break;
+            case RUNE_STAT_ID::SPD:
+                $list = RUNE_STAT_VALUES::SPD;
+                break;
+            case RUNE_STAT_ID::CRR:
+                $list = RUNE_STAT_VALUES::CRR;
+                break;
+            case RUNE_STAT_ID::CRD:
+                $list = RUNE_STAT_VALUES::CRD;
+                break;
+            case RUNE_STAT_ID::ACC:
+                $list = RUNE_STAT_VALUES::ACC;
+                break;
+            case RUNE_STAT_ID::RES:
+                $list = RUNE_STAT_VALUES::RES;
+                break;
+        }
+        $efficiency += floatval($list[$stars][15]) / floatval($list[6][15]);
+        // Innate stat
+        if ($rune->{"prefix_eff"}[0] > 0){
+            $innate_stat = $rune->{"pri_eff"}[0];
+            $innate_stat_value = $rune->{"pri_eff"}[1];
+            $efficiency += floatval($innate_stat_value) / (5 * floatval(RUNE_SUBSTAT_MAX_VALUES::SUBSTAT[$innate_stat][6]));
+        }
+        // Substats
+        for ($i = 0; $i <= 3; $i ++){
+            if (sizeof($rune->{"sec_eff"}) > $i){
+                $substat = $rune->{"sec_eff"}[$i][0];
+                $efficiency += floatval(RUNE_SUBSTAT_MAX_VALUES::SUBSTAT[$substat][$stars]) / (5 * floatval(RUNE_SUBSTAT_MAX_VALUES::SUBSTAT[$substat][6]));
+            }
+        }
+        $efficiency = $efficiency / 2.8 * 100;
+        if ($efficiency > 100){
+            $efficiency = 100;
+        }
+        return $efficiency;
+    }
+
+    /**
+     * Parses the JSONs relateds to a cairos run and saves the data.
+     *
+     * @param SQLiteConn $db Database connection.
+     * @param int $uid Player UID.
+     * @param JSON[] $json List of received JSONs.
+     */
+    function log_cairos_run($db, $uid, $json){
+        // Only the result request and response JSONs are needed:
+        $response = $json["result_res"];
+        if ($json["result_req"] === null){
+            return ("400 Result request is required to parse Cairos runs.");
+        }
+        $request = $json["result_req"];
+
+        // Check if already in DB
+        $dtime = $response->{"tvaluelocal"};
+        $statement = $db->prepare("SELECT count(id) AS c FROM run WHERE uid = :uid AND dtime = :dtime;");
+        $statement->bindValue(":uid", $uid);
+        $statement->bindValue(":dtime", $dtime);
+        if (0 != $statement->execute()->fetchArray(SQLITE3_ASSOC)["c"]){
+            return ("409 Run already in database.");
+        }
+
+        // Get new ID
+        $q = $db->query("SELECT max(id) + 1 AS id FROM run;");
+        $r = $q->fetchArray(SQLITE3_ASSOC);
+        if ($r){
+            $id = $r["id"];
+        }
+        else{
+            $id = 1;
+        }
+
+        // Prepare the "run" insert.
+        $statement = $db->prepare("
+          INSERT INTO run (
+            uid,
+            id,
+            dtime,
+            area_type,
+            area,
+            stage,
+            difficulty,
+            win,
+            time,
+            mana,
+            energy,
+            crystal,
+            guild_points,
+            helper,
+            score,
+            rank
+          ) VALUES (
+            :uid,
+            :id,
+            :dtime,
+            :area_type,
+            :area,
+            :stage,
+            :difficulty,
+            :win,
+            :time,
+            :mana,
+            :energy,
+            :crystal,
+            :guild_points,
+            :helper,
+            :score,
+            :rank
+          );
+        ");
+
+        // Bind data and insert
+        $statement->bindValue(":uid", $uid);
+        $statement->bindValue(":id", $id);
+        $statement->bindValue(":dtime", $dtime);
+        $statement->bindValue(":area_type", AREA_TYPE_ID::CAIROS_DUNGEON);
+        $statement->bindValue(":area", $request->{"dungeon_id"});
+        $statement->bindValue(":stage", $request->{"stage_id"});
+        $statement->bindValue(":difficulty", null);
+        $statement->bindValue(":win", $response->{"win_lose"});
+        $statement->bindValue(":time", $response->{"clear_time"}->{"current_time"});
+        if (array_key_exists("mana", $response->{"reward"})){
+            $statement->bindValue(":mana", $response->{"reward"}->{"mana"});
+        }
+        else{
+            $statement->bindValue(":mana", 0);
+        }
+        if (array_key_exists("energy", $response->{"reward"})){
+            $statement->bindValue(":energy", $response->{"reward"}->{"energy"});
+        }
+        else{
+            $statement->bindValue(":energy", 0);
+        }
+        if (array_key_exists("crystal", $response->{"reward"})){
+            $statement->bindValue(":crystal", $response->{"reward"}->{"crystal"});
+        }
+        else{
+            $statement->bindValue(":crystal", 0);
+        }
+        $statement->bindValue(":guild_points", 0);
+        $statement->bindValue(":helper", 0); // TODO Where is this information??
+        $statement->bindValue(":score", null);
+        $statement->bindValue(":rank", null);
+        $statement->execute();
+
+        // Parse various types of reward
+        if (array_key_exists("changed_item_list", $response)){
+            $reward_type = $response->{"changed_item_list"}[0]->{"type"};
+            switch ($reward_type){
+                case INVENTORY_TYPE_ID::MONSTER: // Unit
+                    $statement = $db->prepare("INSERT INTO run_drop_unit (run, unit) VALUES (:run, :unit);");
+                    $statement->bindValue(":run", $id);
+                    $statement->bindValue(":unit", $response->{"changed_item_list"}[0]->{"view"}->{"item_master_id"});
+                    $statement->execute();
+                    break;
+                case INVENTORY_TYPE_ID::SCROLL:
+                case INVENTORY_TYPE_ID::ESSENCES:
+                    $statement = $db->prepare("INSERT INTO run_drop_item (run, item, amount) VALUES (:run, :item, :amount);");
+                    $statement->bindValue(":run", $id);
+                    $statement->bindValue(":item", $response->{"changed_item_list"}[0]->{"view"}->{"item_master_id"});
+                    $statement->bindValue(":amount", $response->{"changed_item_list"}[0]->{"view"}->{"item_quantity"});
+                    $statement->execute();
+                    break;
+                case INVENTORY_TYPE_ID::RUNE:
+                    $rune = $response->{"changed_item_list"}[0]->{"info"};
+                    $statement = $db->prepare("
+                      INSERT INTO run_drop_rune (
+                        run,
+                        id,
+                        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
+                      ) VALUES (
+                        :run,
+                        :id,
+                        :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
+                      );
+                    ");
+                    $statement->bindValue(":run", $id);
+                    $statement->bindValue(":id", $rune->{"rune_id"});
+                    $statement->bindValue(":type", $rune->{"set_id"});
+                    $statement->bindValue(":slot", $rune->{"slot_no"});
+                    $statement->bindValue(":value", $rune->{"sell_value"});
+                    if ($rune->{"class"} > 10){
+                        // Ancient rune
+                        $statement->bindValue(":ancient", 1);
+                        $statement->bindValue(":stars", intval($rune->{"class"}) - 10);
+                        $statement->bindValue(":quality", intval($rune->{"rank"}) - 10);
+                    }
+                    else{
+                        // Non Ancient rune
+                        $statement->bindValue(":ancient", 0);
+                        $statement->bindValue(":stars", $rune->{"class"});
+                        $statement->bindValue(":quality", $rune->{"rank"});
+                    }
+                    $efficiency = calculate_efficiency($rune);
+                    $max_efficiency = calculate_maximum_efficiency($rune);
+                    $statement->bindValue(":efficiency", $efficiency);
+                    $statement->bindValue(":max_efficiency", $max_efficiency);
+                    $statement->bindValue(":main_stat", $rune->{"pri_eff"}[0]);
+                    $statement->bindValue(":main_stat_value", $rune->{"pri_eff"}[1]);
+                    if ($rune->{"prefix_eff"}[0] > 0){
+                        $statement->bindValue(":innate_stat", $rune->{"prefix_eff"}[0]);
+                        $statement->bindValue(":innate_stat_value", $rune->{"prefix_eff"}[1]);
+                    }
+                    else{
+                        $statement->bindValue(":innate_stat", null);
+                        $statement->bindValue(":innate_stat_value", 0);
+                    }
+                    if (sizeof($rune->{"sec_eff"}) > 0){
+                        $statement->bindValue(":substat_1", $rune->{"sec_eff"}[0][0]);
+                        $statement->bindValue(":substat_1_value", $rune->{"sec_eff"}[0][1]);
+                    }
+                    else{
+                        $statement->bindValue(":substat_1", null);
+                        $statement->bindValue(":substat_1_value", 0);
+                    }
+                    if (sizeof($rune->{"sec_eff"}) > 1){
+                        $statement->bindValue(":substat_2", $rune->{"sec_eff"}[1][0]);
+                        $statement->bindValue(":substat_2_value", $rune->{"sec_eff"}[1][1]);
+                    }
+                    else{
+                        $statement->bindValue(":substat_2", null);
+                        $statement->bindValue(":substat_2_value", 0);
+                    }
+                    if (sizeof($rune->{"sec_eff"}) > 2){
+                        $statement->bindValue(":substat_3", $rune->{"sec_eff"}[2][0]);
+                        $statement->bindValue(":substat_3_value", $rune->{"sec_eff"}[2][1]);
+                    }
+                    else{
+                        $statement->bindValue(":substat_3", null);
+                        $statement->bindValue(":substat_3_value", 0);
+                    }
+                    if (sizeof($rune->{"sec_eff"}) > 3){
+                        $statement->bindValue(":substat_4", $rune->{"sec_eff"}[3][0]);
+                        $statement->bindValue(":substat_4_value", $rune->{"sec_eff"}[3][1]);
+                    }
+                    else{
+                        $statement->bindValue(":substat_4", null);
+                        $statement->bindValue(":substat_4_value", 0);
+                    }
+                    $statement->execute();
+                    break;
+            }
+        }
+
+        // Parse party
+        $leader = 1;
+        foreach ($response->{"unit_list"} as $unit){
+            $statement = $db->prepare("
+              INSERT INTO run_party 
+                (run, unit, k_unit, leader, front)
+              VALUES
+                (:run, :unit, :k_unit, :leader, :front);"
+            );
+            $statement->bindValue(":run", $id);
+            $statement->bindValue(":unit", $unit->{"unit_id"});
+            $statement->bindValue(":k_unit", $unit->{"unit_master_id"});
+            $statement->bindValue(":leader", $leader);
+            $leader = 0;
+            $statement->bindValue(":front", 0);
+            $statement->execute();
+        }
+
+        // Return success
+        return "201 Created.";
+    }
+
+    global $db;
+
+    try{
+
+        header("Content-type: application/json; charset=utf-8");
+
+        // Get put data
+        parse_str(file_get_contents("php://input"), $_POST);
+
+        // Check API key.
+        if (!array_key_exists("key", $_POST)){
+            header("HTTP/1.1 400 API key not received.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 400 API key not received.");
+            return 400;
+        }
+        $key = $_POST["key"];
+
+        // Get all JSON files
+        $json = [
+            "start_req" => null,
+            "start_res" => null,
+            "result_req" => null,
+            "result_res" => null,
+        ];
+
+        // Result response - mandatory
+        if (!array_key_exists("result_response", $_POST)){
+            header("HTTP/1.1 400 No result response data received.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 400 No result response data received.");
+            return 400;
+        }
+        $data_json = json_decode($_POST["result_response"]);
+        if ($data_json === null){
+            header("HTTP/1.1 400 Result response data is no valid JSON.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 400 Result response data is no valid JSON.");
+            return 400;
+        }
+        $json["result_res"] = $data_json;
+
+        // Result request - optional
+        if (array_key_exists("result_request", $_POST)){
+            $data_raw = $_POST["result_request"];
+            if ($data_raw != null && $data_raw != false){
+                $data_json = json_decode($data_raw);
+                if ($data_json != null){
+                    $json["result_req"] = $data_json;
+                }
+            }
+        }
+        // Start result - optional
+        if (array_key_exists("start_response", $_POST)){
+            $data_raw = $_POST["start_response"];
+            if ($data_raw != null && $data_raw != false){
+                $data_json = json_decode($data_raw);
+                if ($data_json != null){
+                    $json["start_res"] = $data_json;
+                }
+            }
+        }
+        // Start result - optional
+        if (array_key_exists("start_request", $_POST)){
+            $data_raw = $_POST["start_request"];
+            if ($data_raw != null && $data_raw != false){
+                $data_json = json_decode($data_raw);
+                if ($data_json != null){
+                    $json["start_req"] = $data_json;
+                }
+            }
+        }
+
+        // Extract basic data
+        $uid = $json["result_res"]->{"wizard_info"}->{"wizard_id"};
+        $command = $json["result_res"]->{"command"};
+
+        // Authenticate
+        $statement = $db->prepare("SELECT COUNT(uid) AS c FROM player WHERE uid = :uid AND api_key = :api_key';");
+        $statement->bindValue(":uid", $uid);
+        $statement->bindValue(":api_key", $key);
+        if (1 != $statement->execute()->fetchArray(SQLITE3_ASSOC)["c"]){
+            header("HTTP/1.1 401 Invalid credentials.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 401 Invalid credentials.");
+            return 401;
+        }
+
+        // Check for implemented command
+        $accepted_commands = [
+            "BattleDungeonResult_V2" // Dungeon RUN
+        ];
+        if (!in_array($command, $accepted_commands)){
+            header("HTTP/1.1 405 Command not implemented.");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 405 Command not implemented.");
+            return 405;
+        }
+
+        // Run parsing function
+        $result = "500 Unknown Error";
+        switch($command){
+            case "BattleDungeonResult_V2":
+                $result = log_cairos_run($db, $uid, $json);
+                break;
+        }
+
+        // Process result
+        $status_code = intval(substr($result, 0, 3));
+        $status_message = substr($result, 4);
+        if ($status_code == 0 || $status_code < 100 || $status_code > 599){
+            header("HTTP/1.1 500 Unknown error.");
+            syslog(LOG_ERR, "[APIv3] HTTP/1.1 500 Unknown error.");
+            return 500;
+        }
+        else{
+            header("HTTP/1.1 $status_code $status_message");
+            syslog(LOG_INFO, "[APIv3] HTTP/1.1 $status_code $status_message");
+            return $status_code;
+        }
+    }
+    catch(Exception $e) {
+        header("HTTP/1.1 500 Unexpeced error: " . $e->getMessage());
+        syslog(LOG_ERR, "[APIv3] HTTP/1.1 500 Unexpected error: " . $e->getMessage());
+        return 500;
+    }
+?>

+ 108 - 0
application/API/v3/units.php

@@ -0,0 +1,108 @@
+<?php
+    /**
+     * Unit api.
+     *
+     * Exposes an API to get a list of units of a player, or a unit details.
+     * Used POST parameters are:
+     *  - key: User API key. If not, only public user's unit can be seen.
+     *
+     * @category API
+     * @magic $params URL parameters:
+     *  1: User ID
+     *  2: Unit ID (optional)
+     */
+
+    global $db;
+
+    try{
+
+        if (count($params) <= 1){
+            http_response_code(400);
+            return 400;
+        }
+        $uid = $params[1];
+        $id = null;
+
+        if (count($params) > 2){
+            $id = $params[2];
+        }
+
+        // Check API key or public profile.
+        if (isset($_POST['key'])){
+            $key = filter_input(INPUT_POST, 'key');
+        }
+        else{
+            $key = "";
+        }
+        $s = "SELECT COUNT(uid) AS c FROM player WHERE uid = $uid AND (public = 1 OR api_key = '$key');";
+        if (1 != $db->query($s)->fetchArray(SQLITE3_ASSOC)["c"]){
+            http_response_code(401);
+            return 401;
+        }
+
+        // No unit ID specified, show a list.
+        if ($id == null){
+            $units = [];
+            $s = "SELECT id, '" . URL::BASE . "API/v2/units/$uid/' || id AS uri FROM unit WHERE uid = '$uid'";
+            $q = $db->query($s);
+            $content = false;
+            while ($r = $q->fetchArray(SQLITE3_ASSOC)){
+                array_push($units, $r);
+                $content = true;
+            }
+            if ($content == true){
+                header('Content-Type: application/json');
+                echo json_encode($units);
+                http_response_code(200);
+                return 200;
+            }
+            else{
+                http_response_code(204);
+                return 204;
+            }
+        }
+
+        // Specific unit, show details.
+        else{
+            $s = "SELECT * FROM unit WHERE uid = '$uid' AND id = '$id'";
+            $q = $db->query($s);
+            if($r = $q->fetchArray(SQLITE3_ASSOC)){
+                $unit = $r;
+
+                // Runes
+                $unit["runes"] = [];
+                $s = "SELECT * FROM rune WHERE assigned_to = '" . $unit["id"] . "' ORDER BY slot;";
+                $q = $db->query($s);
+                while ($r = $q->fetchArray(SQLITE3_ASSOC)){
+                    $rune = $r;
+                    unset($rune["uid"]); # Not needed
+                    unset($rune["assigned_to"]); # Not needed
+                    array_push($unit["runes"], $rune);
+                }
+
+                // Skills
+                $unit["skills"] = [];
+                $s = "SELECT skill, level FROM unit_skill WHERE unit = '" . $unit["id"] . "';";
+                $q = $db->query($s);
+                while ($r = $q->fetchArray(SQLITE3_ASSOC)){
+                    array_push($unit["skills"], $r);
+                }
+
+                header('Content-Type: application/json');
+                echo json_encode($unit);
+                http_response_code(200);
+                return 200;
+            }
+            else{
+                http_response_code(404);
+                return 404;
+            }
+        }
+
+    }
+    catch(Exception $e) {
+        error_log("Unknown error fetching units: " . $e->getMessage());
+        http_response_code(500);
+        return 500;
+    }
+?>

+ 1355 - 0
application/Constant.php

@@ -2763,6 +2763,1361 @@
         ];
     };
 
+    /**
+     * Rune main stat values constants.
+     *
+     * Includes the value of the main stats for type, stars, and level.
+     *
+     * @category Data
+     */
+    final class RUNE_SUBSTAT_MAX_VALUES{
+    
+        const SUBSTAT = [
+            RUNE_STAT_ID::HP => [
+                "1" => 300,
+                "2" => 525,
+                "3" => 825,
+                "4" => 1125,
+                "5" => 1500,
+                "6" => 1875
+            ],
+            RUNE_STAT_ID::HP_P => [
+                "1" => 10,
+                "2" => 15,
+                "3" => 25,
+                "4" => 30,
+                "5" => 35,
+                "6" => 40
+            ],
+            RUNE_STAT_ID::ATK => [
+                "1" => 20,
+                "2" => 25,
+                "3" => 40,
+                "4" => 50,
+                "5" => 75,
+                "6" => 100
+            ],
+            RUNE_STAT_ID::ATK_P => [
+                "1" => 10,
+                "2" => 15,
+                "3" => 25,
+                "4" => 30,
+                "5" => 35,
+                "6" => 40
+            ],
+            RUNE_STAT_ID::DEF => [
+                "1" => 20,
+                "2" => 25,
+                "3" => 40,
+                "4" => 50,
+                "5" => 75,
+                "6" => 100
+            ],
+            RUNE_STAT_ID::DEF_P => [
+                "1" => 10,
+                "2" => 15,
+                "3" => 25,
+                "4" => 30,
+                "5" => 35,
+                "6" => 40
+            ],
+            RUNE_STAT_ID::SPD => [
+                "1" => 5,
+                "2" => 10,
+                "3" => 15,
+                "4" => 20,
+                "5" => 25,
+                "6" => 30
+            ],
+            RUNE_STAT_ID::CRR => [
+                "1" => 5,
+                "2" => 10,
+                "3" => 15,
+                "4" => 20,
+                "5" => 25,
+                "6" => 30
+            ],
+            RUNE_STAT_ID::CRD => [
+                "1" => 10,
+                "2" => 15,
+                "3" => 20,
+                "4" => 25,
+                "5" => 30,
+                "6" => 35
+            ],
+            RUNE_STAT_ID::RES => [
+                "1" => 10,
+                "2" => 15,
+                "3" => 25,
+                "4" => 30,
+                "5" => 35,
+                "6" => 40
+            ],
+            RUNE_STAT_ID::ACC => [
+                "1" => 10,
+                "2" => 15,
+                "3" => 25,
+                "4" => 30,
+                "5" => 35,
+                "6" => 40
+            ]
+        ];
+
+        /**
+         * Values of the HP main stat [stars][level]
+         */
+        const HP = [
+            "1" => [
+                "0" => 40,
+                "1" => 85,
+                "2" => 130,
+                "3" => 175,
+                "4" => 220,
+                "5" => 265,
+                "6" => 310,
+                "7" => 355,
+                "8" => 400,
+                "9" => 445,
+                "10" => 490,
+                "11" => 535,
+                "12" => 580,
+                "13" => 625,
+                "14" => 670,
+                "15" => 804
+            ],
+            "2" => [
+                "0" => 70,
+                "1" => 130,
+                "2" => 190,
+                "3" => 250,
+                "4" => 310,
+                "5" => 370,
+                "6" => 430,
+                "7" => 490,
+                "8" => 550,
+                "9" => 610,
+                "10" => 670,
+                "11" => 730,
+                "12" => 790,
+                "13" => 850,
+                "14" => 910,
+                "15" => 1092
+            ],
+            "3" => [
+                "0" => 100,
+                "1" => 175,
+                "2" => 250,
+                "3" => 325,
+                "4" => 400,
+                "5" => 475,
+                "6" => 550,
+                "7" => 625,
+                "8" => 700,
+                "9" => 775,
+                "10" => 850,
+                "11" => 925,
+                "12" => 1000,
+                "13" => 1075,
+                "14" => 1150,
+                "15" => 1380
+            ],
+            "4" => [
+                "0" => 160,
+                "1" => 250,
+                "2" => 340,
+                "3" => 430,
+                "4" => 520,
+                "5" => 610,
+                "6" => 700,
+                "7" => 790,
+                "8" => 880,
+                "9" => 970,
+                "10" => 1060,
+                "11" => 1150,
+                "12" => 1240,
+                "13" => 1330,
+                "14" => 1420,
+                "15" => 1704
+            ],
+            "5" => [
+                "0" => 270,
+                "1" => 375,
+                "2" => 480,
+                "3" => 585,
+                "4" => 690,
+                "5" => 795,
+                "6" => 900,
+                "7" => 1005,
+                "8" => 1110,
+                "9" => 1215,
+                "10" => 1320,
+                "11" => 1425,
+                "12" => 1530,
+                "13" => 1635,
+                "14" => 1740,
+                "15" => 2088
+            ],
+            "6" => [
+                "0" => 360,
+                "1" => 480,
+                "2" => 600,
+                "3" => 720,
+                "4" => 840,
+                "5" => 960,
+                "6" => 1080,
+                "7" => 1200,
+                "8" => 1320,
+                "9" => 1440,
+                "10" => 1560,
+                "11" => 1680,
+                "12" => 1800,
+                "13" => 1920,
+                "14" => 2040,
+                "15" => 2448
+            ]
+        ];
+        
+        /**
+         * Values of the HP_P main stat[stars][level]
+         */
+        const HP_P = [
+            "1" => [
+                "0" => 1,
+                "1" => 2,
+                "2" => 3,
+                "3" => 4,
+                "4" => 5,
+                "5" => 6,
+                "6" => 7,
+                "7" => 8,
+                "8" => 9,
+                "9" => 10,
+                "10" => 11,
+                "11" => 12,
+                "12" => 13,
+                "13" => 14,
+                "14" => 15,
+                "15" => 18
+            ],
+            "2" => [
+                "0" => 2,
+                "1" => 3,
+                "2" => 4,
+                "3" => 5,
+                "4" => 6,
+                "5" => 7,
+                "6" => 8,
+                "7" => 9,
+                "8" => 10,
+                "9" => 11,
+                "10" => 12,
+                "11" => 13,
+                "12" => 14,
+                "13" => 15,
+                "14" => 16,
+                "15" => 19
+            ],
+            "3" => [
+                "0" => 4,
+                "1" => 6,
+                "2" => 8,
+                "3" => 10,
+                "4" => 12,
+                "5" => 14,
+                "6" => 16,
+                "7" => 18,
+                "8" => 20,
+                "9" => 22,
+                "10" => 24,
+                "11" => 26,
+                "12" => 28,
+                "13" => 30,
+                "14" => 32,
+                "15" => 38
+            ],
+            "4" => [
+                "0" => 5,
+                "1" => 7,
+                "2" => 9,
+                "3" => 11,
+                "4" => 13,
+                "5" => 16,
+                "6" => 18,
+                "7" => 20,
+                "8" => 22,
+                "9" => 24,
+                "10" => 27,
+                "11" => 29,
+                "12" => 31,
+                "13" => 33,
+                "14" => 36,
+                "15" => 43
+            ],
+            "5" => [
+                "0" => 8,
+                "1" => 10,
+                "2" => 12,
+                "3" => 15,
+                "4" => 17,
+                "5" => 20,
+                "6" => 22,
+                "7" => 24,
+                "8" => 27,
+                "9" => 29,
+                "10" => 32,
+                "11" => 34,
+                "12" => 37,
+                "13" => 40,
+                "14" => 43,
+                "15" => 51
+            ],
+            "6" => [
+                "0" => 11,
+                "1" => 14,
+                "2" => 17,
+                "3" => 20,
+                "4" => 23,
+                "5" => 26,
+                "6" => 29,
+                "7" => 32,
+                "8" => 35,
+                "9" => 38,
+                "10" => 41,
+                "11" => 44,
+                "12" => 47,
+                "13" => 50,
+                "14" => 53,
+                "15" => 63
+            ]
+        ];
+
+        /**
+         * Values of the ATK main stat[stars][level]
+         */
+        const ATK = [
+            "1" => [
+                "0" => 3,
+                "1" => 6,
+                "2" => 9,
+                "3" => 12,
+                "4" => 15,
+                "5" => 18,
+                "6" => 21,
+                "7" => 24,
+                "8" => 27,
+                "9" => 30,
+                "10" => 33,
+                "11" => 36,
+                "12" => 39,
+                "13" => 42,
+                "14" => 45,
+                "15" => 54
+            ],
+            "2" => [
+                "0" => 5,
+                "1" => 9,
+                "2" => 13,
+                "3" => 17,
+                "4" => 21,
+                "5" => 25,
+                "6" => 29,
+                "7" => 33,
+                "8" => 37,
+                "9" => 41,
+                "10" => 45,
+                "11" => 49,
+                "12" => 53,
+                "13" => 57,
+                "14" => 61,
+                "15" => 73
+            ],
+            "3" => [
+                "0" => 7,
+                "1" => 12,
+                "2" => 17,
+                "3" => 22,
+                "4" => 27,
+                "5" => 32,
+                "6" => 37,
+                "7" => 42,
+                "8" => 47,
+                "9" => 52,
+                "10" => 57,
+                "11" => 62,
+                "12" => 67,
+                "13" => 72,
+                "14" => 77,
+                "15" => 92
+            ],
+            "4" => [
+                "0" => 10,
+                "1" => 16,
+                "2" => 22,
+                "3" => 28,
+                "4" => 34,
+                "5" => 40,
+                "6" => 46,
+                "7" => 52,
+                "8" => 58,
+                "9" => 64,
+                "10" => 70,
+                "11" => 76,
+                "12" => 82,
+                "13" => 88,
+                "14" => 94,
+                "15" => 112
+            ],
+            "5" => [
+                "0" => 15,
+                "1" => 22,
+                "2" => 29,
+                "3" => 36,
+                "4" => 43,
+                "5" => 50,
+                "6" => 57,
+                "7" => 64,
+                "8" => 71,
+                "9" => 78,
+                "10" => 85,
+                "11" => 92,
+                "12" => 99,
+                "13" => 106,
+                "14" => 113,
+                "15" => 135
+            ],
+            "6" => [
+                "0" => 22,
+                "1" => 30,
+                "2" => 38,
+                "3" => 46,
+                "4" => 54,
+                "5" => 62,
+                "6" => 70,
+                "7" => 78,
+                "8" => 86,
+                "9" => 94,
+                "10" => 102,
+                "11" => 110,
+                "12" => 118,
+                "13" => 126,
+                "14" => 134,
+                "15" => 160
+            ]
+        ];
+
+        /**
+         * Values of the ATK_P main stat[stars][level]
+         */
+        const ATK_P = [
+            "1" => [
+                "0" => 1,
+                "1" => 2,
+                "2" => 3,
+                "3" => 4,
+                "4" => 5,
+                "5" => 6,
+                "6" => 7,
+                "7" => 8,
+                "8" => 9,
+                "9" => 10,
+                "10" => 11,
+                "11" => 12,
+                "12" => 13,
+                "13" => 14,
+                "14" => 15,
+                "15" => 18
+            ],
+            "2" => [
+                "0" => 2,
+                "1" => 3,
+                "2" => 4,
+                "3" => 5,
+                "4" => 6,
+                "5" => 7,
+                "6" => 8,
+                "7" => 9,
+                "8" => 10,
+                "9" => 11,
+                "10" => 12,
+                "11" => 13,
+                "12" => 14,
+                "13" => 15,
+                "14" => 16,
+                "15" => 19
+            ],
+            "3" => [
+                "0" => 4,
+                "1" => 6,
+                "2" => 8,
+                "3" => 10,
+                "4" => 12,
+                "5" => 14,
+                "6" => 16,
+                "7" => 18,
+                "8" => 20,
+                "9" => 22,
+                "10" => 24,
+                "11" => 26,
+                "12" => 28,
+                "13" => 30,
+                "14" => 32,
+                "15" => 38
+            ],
+            "4" => [
+                "0" => 5,
+                "1" => 7,
+                "2" => 9,
+                "3" => 11,
+                "4" => 13,
+                "5" => 16,
+                "6" => 18,
+                "7" => 20,
+                "8" => 22,
+                "9" => 24,
+                "10" => 27,
+                "11" => 29,
+                "12" => 31,
+                "13" => 33,
+                "14" => 36,
+                "15" => 43
+            ],
+            "5" => [
+                "0" => 8,
+                "1" => 10,
+                "2" => 12,
+                "3" => 15,
+                "4" => 17,
+                "5" => 20,
+                "6" => 22,
+                "7" => 24,
+                "8" => 27,
+                "9" => 29,
+                "10" => 32,
+                "11" => 34,
+                "12" => 37,
+                "13" => 40,
+                "14" => 43,
+                "15" => 51
+            ],
+            "6" => [
+                "0" => 11,
+                "1" => 14,
+                "2" => 17,
+                "3" => 20,
+                "4" => 23,
+                "5" => 26,
+                "6" => 29,
+                "7" => 32,
+                "8" => 35,
+                "9" => 38,
+                "10" => 41,
+                "11" => 44,
+                "12" => 47,
+                "13" => 50,
+                "14" => 53,
+                "15" => 63
+            ]
+        ];
+        
+        /**
+         * Values of the DEF main stat[stars][level]
+         */
+        const DEF = [
+            "1" => [
+                "0" => 3,
+                "1" => 6,
+                "2" => 9,
+                "3" => 12,
+                "4" => 15,
+                "5" => 18,
+                "6" => 21,
+                "7" => 24,
+                "8" => 27,
+                "9" => 30,
+                "10" => 33,
+                "11" => 36,
+                "12" => 39,
+                "13" => 42,
+                "14" => 45,
+                "15" => 54
+            ],
+            "2" => [
+                "0" => 5,
+                "1" => 9,
+                "2" => 13,
+                "3" => 17,
+                "4" => 21,
+                "5" => 25,
+                "6" => 29,
+                "7" => 33,
+                "8" => 37,
+                "9" => 41,
+                "10" => 45,
+                "11" => 49,
+                "12" => 53,
+                "13" => 57,
+                "14" => 61,
+                "15" => 73
+            ],
+            "3" => [
+                "0" => 7,
+                "1" => 12,
+                "2" => 17,
+                "3" => 22,
+                "4" => 27,
+                "5" => 32,
+                "6" => 37,
+                "7" => 42,
+                "8" => 47,
+                "9" => 52,
+                "10" => 57,
+                "11" => 62,
+                "12" => 67,
+                "13" => 72,
+                "14" => 77,
+                "15" => 92
+            ],
+            "4" => [
+                "0" => 10,
+                "1" => 16,
+                "2" => 22,
+                "3" => 28,
+                "4" => 34,
+                "5" => 40,
+                "6" => 46,
+                "7" => 52,
+                "8" => 58,
+                "9" => 64,
+                "10" => 70,
+                "11" => 76,
+                "12" => 82,
+                "13" => 88,
+                "14" => 94,
+                "15" => 112
+            ],
+            "5" => [
+                "0" => 15,
+                "1" => 22,
+                "2" => 29,
+                "3" => 36,
+                "4" => 43,
+                "5" => 50,
+                "6" => 57,
+                "7" => 64,
+                "8" => 71,
+                "9" => 78,
+                "10" => 85,
+                "11" => 92,
+                "12" => 99,
+                "13" => 106,
+                "14" => 113,
+                "15" => 135
+            ],
+            "6" => [
+                "0" => 22,
+                "1" => 30,
+                "2" => 38,
+                "3" => 46,
+                "4" => 54,
+                "5" => 62,
+                "6" => 70,
+                "7" => 78,
+                "8" => 86,
+                "9" => 94,
+                "10" => 102,
+                "11" => 110,
+                "12" => 118,
+                "13" => 126,
+                "14" => 134,
+                "15" => 160
+            ]
+        ];
+        
+        /**
+         * Values of the DEF_P main stat[stars][level]
+         */
+        const DEF_P = [
+            "1" => [
+                "0" => 1,
+                "1" => 2,
+                "2" => 3,
+                "3" => 4,
+                "4" => 5,
+                "5" => 6,
+                "6" => 7,
+                "7" => 8,
+                "8" => 9,
+                "9" => 10,
+                "10" => 11,
+                "11" => 12,
+                "12" => 13,
+                "13" => 14,
+                "14" => 15,
+                "15" => 18
+            ],
+            "2" => [
+                "0" => 2,
+                "1" => 3,
+                "2" => 4,
+                "3" => 5,
+                "4" => 6,
+                "5" => 7,
+                "6" => 8,
+                "7" => 9,
+                "8" => 10,
+                "9" => 11,
+                "10" => 12,
+                "11" => 13,
+                "12" => 14,
+                "13" => 15,
+                "14" => 16,
+                "15" => 19
+            ],
+            "3" => [
+                "0" => 4,
+                "1" => 6,
+                "2" => 8,
+                "3" => 10,
+                "4" => 12,
+                "5" => 14,
+                "6" => 16,
+                "7" => 18,
+                "8" => 20,
+                "9" => 22,
+                "10" => 24,
+                "11" => 26,
+                "12" => 28,
+                "13" => 30,
+                "14" => 32,
+                "15" => 38
+            ],
+            "4" => [
+                "0" => 5,
+                "1" => 7,
+                "2" => 9,
+                "3" => 11,
+                "4" => 13,
+                "5" => 16,
+                "6" => 18,
+                "7" => 20,
+                "8" => 22,
+                "9" => 24,
+                "10" => 27,
+                "11" => 29,
+                "12" => 31,
+                "13" => 33,
+                "14" => 36,
+                "15" => 43
+            ],
+            "5" => [
+                "0" => 8,
+                "1" => 10,
+                "2" => 12,
+                "3" => 15,
+                "4" => 17,
+                "5" => 20,
+                "6" => 22,
+                "7" => 24,
+                "8" => 27,
+                "9" => 29,
+                "10" => 32,
+                "11" => 34,
+                "12" => 37,
+                "13" => 40,
+                "14" => 43,
+                "15" => 51
+            ],
+            "6" => [
+                "0" => 11,
+                "1" => 14,
+                "2" => 17,
+                "3" => 20,
+                "4" => 23,
+                "5" => 26,
+                "6" => 29,
+                "7" => 32,
+                "8" => 35,
+                "9" => 38,
+                "10" => 41,
+                "11" => 44,
+                "12" => 47,
+                "13" => 50,
+                "14" => 53,
+                "15" => 63
+            ]
+        ];
+
+        /**
+         * Values of the SPD main stat[stars][level]
+         */
+        const SPD = [
+            "1" => [
+                "0" => 1,
+                "1" => 2,
+                "2" => 3,
+                "3" => 4,
+                "4" => 5,
+                "5" => 6,
+                "6" => 7,
+                "7" => 8,
+                "8" => 9,
+                "9" => 10,
+                "10" => 11,
+                "11" => 12,
+                "12" => 13,
+                "13" => 14,
+                "14" => 15,
+                "15" => 18
+            ],
+            "2" => [
+                "0" => 2,
+                "1" => 3,
+                "2" => 4,
+                "3" => 5,
+                "4" => 6,
+                "5" => 7,
+                "6" => 8,
+                "7" => 9,
+                "8" => 10,
+                "9" => 11,
+                "10" => 12,
+                "11" => 13,
+                "12" => 14,
+                "13" => 15,
+                "14" => 16,
+                "15" => 19
+            ],
+            "3" => [
+                "0" => 3,
+                "1" => 4,
+                "2" => 5,
+                "3" => 6,
+                "4" => 8,
+                "5" => 9,
+                "6" => 10,
+                "7" => 12,
+                "8" => 13,
+                "9" => 14,
+                "10" => 16,
+                "11" => 17,
+                "12" => 18,
+                "13" => 19,
+                "14" => 21,
+                "15" => 25
+            ],
+            "4" => [
+                "0" => 4,
+                "1" => 5,
+                "2" => 7,
+                "3" => 8,
+                "4" => 10,
+                "5" => 11,
+                "6" => 13,
+                "7" => 14,
+                "8" => 16,
+                "9" => 17,
+                "10" => 19,
+                "11" => 20,
+                "12" => 22,
+                "13" => 23,
+                "14" => 25,
+                "15" => 30
+            ],
+            "5" => [
+                "0" => 5,
+                "1" => 7,
+                "2" => 9,
+                "3" => 11,
+                "4" => 13,
+                "5" => 15,
+                "6" => 17,
+                "7" => 19,
+                "8" => 21,
+                "9" => 23,
+                "10" => 25,
+                "11" => 27,
+                "12" => 29,
+                "13" => 31,
+                "14" => 33,
+                "15" => 39
+            ],
+            "6" => [
+                "0" => 7,
+                "1" => 9,
+                "2" => 11,
+                "3" => 13,
+                "4" => 15,
+                "5" => 17,
+                "6" => 19,
+                "7" => 21,
+                "8" => 23,
+                "9" => 25,
+                "10" => 27,
+                "11" => 29,
+                "12" => 31,
+                "13" => 33,
+                "14" => 35,
+                "15" => 42
+            ]
+        ];
+
+        /**
+         * Values of the CRR main stat[stars][level]
+         */
+        const CRR = [
+            "1" => [
+                "0" => 1,
+                "1" => 2,
+                "2" => 3,
+                "3" => 4,
+                "4" => 5,
+                "5" => 6,
+                "6" => 7,
+                "7" => 8,
+                "8" => 9,
+                "9" => 10,
+                "10" => 11,
+                "11" => 12,
+                "12" => 13,
+                "13" => 14,
+                "14" => 15,
+                "15" => 18
+            ],
+            "2" => [
+                "0" => 2,
+                "1" => 3,
+                "2" => 4,
+                "3" => 5,
+                "4" => 6,
+                "5" => 7,
+                "6" => 8,
+                "7" => 9,
+                "8" => 10,
+                "9" => 11,
+                "10" => 12,
+                "11" => 13,
+                "12" => 14,
+                "13" => 15,
+                "14" => 16,
+                "15" => 19
+            ],
+            "3" => [
+                "0" => 3,
+                "1" => 5,
+                "2" => 7,
+                "3" => 9,
+                "4" => 11,
+                "5" => 13,
+                "6" => 15,
+                "7" => 17,
+                "8" => 19,
+                "9" => 21,
+                "10" => 23,
+                "11" => 25,
+                "12" => 27,
+                "13" => 29,
+                "14" => 31,
+                "15" => 37
+            ],
+            "4" => [
+                "0" => 4,
+                "1" => 6,
+                "2" => 8,
+                "3" => 11,
+                "4" => 13,
+                "5" => 15,
+                "6" => 17,
+                "7" => 19,
+                "8" => 22,
+                "9" => 24,
+                "10" => 26,
+                "11" => 28,
+                "12" => 30,
+                "13" => 33,
+                "14" => 35,
+                "15" => 41
+            ],
+            "5" => [
+                "0" => 5,
+                "1" => 7,
+                "2" => 10,
+                "3" => 12,
+                "4" => 15,
+                "5" => 17,
+                "6" => 19,
+                "7" => 22,
+                "8" => 24,
+                "9" => 27,
+                "10" => 29,
+                "11" => 31,
+                "12" => 34,
+                "13" => 36,
+                "14" => 39,
+                "15" => 47
+            ],
+            "6" => [
+                "0" => 7,
+                "1" => 10,
+                "2" => 13,
+                "3" => 16,
+                "4" => 19,
+                "5" => 22,
+                "6" => 25,
+                "7" => 28,
+                "8" => 31,
+                "9" => 34,
+                "10" => 37,
+                "11" => 40,
+                "12" => 43,
+                "13" => 46,
+                "14" => 49,
+                "15" => 58
+            ]
+        ];
+
+        /**
+         * Values of the CRD main stat[stars][level]
+         */
+        const CRD = [
+            "1" => [
+                "0" => 2,
+                "1" => 3,
+                "2" => 4,
+                "3" => 5,
+                "4" => 6,
+                "5" => 7,
+                "6" => 8,
+                "7" => 9,
+                "8" => 10,
+                "9" => 11,
+                "10" => 12,
+                "11" => 13,
+                "12" => 14,
+                "13" => 15,
+                "14" => 16,
+                "15" => 19
+            ],
+            "2" => [
+                "0" => 3,
+                "1" => 5,
+                "2" => 7,
+                "3" => 9,
+                "4" => 11,
+                "5" => 13,
+                "6" => 15,
+                "7" => 17,
+                "8" => 19,
+                "9" => 21,
+                "10" => 23,
+                "11" => 25,
+                "12" => 27,
+                "13" => 29,
+                "14" => 31,
+                "15" => 37
+            ],
+            "3" => [
+                "0" => 4,
+                "1" => 6,
+                "2" => 9,
+                "3" => 11,
+                "4" => 13,
+                "5" => 16,
+                "6" => 18,
+                "7" => 20,
+                "8" => 22,
+                "9" => 25,
+                "10" => 27,
+                "11" => 29,
+                "12" => 32,
+                "13" => 34,
+                "14" => 36,
+                "15" => 43
+            ],
+            "4" => [
+                "0" => 6,
+                "1" => 9,
+                "2" => 12,
+                "3" => 15,
+                "4" => 18,
+                "5" => 21,
+                "6" => 24,
+                "7" => 27,
+                "8" => 30,
+                "9" => 33,
+                "10" => 36,
+                "11" => 39,
+                "12" => 42,
+                "13" => 45,
+                "14" => 48,
+                "15" => 57
+            ],
+            "5" => [
+                "0" => 8,
+                "1" => 11,
+                "2" => 15,
+                "3" => 18,
+                "4" => 21,
+                "5" => 25,
+                "6" => 28,
+                "7" => 31,
+                "8" => 34,
+                "9" => 38,
+                "10" => 41,
+                "11" => 44,
+                "12" => 48,
+                "13" => 51,
+                "14" => 54,
+                "15" => 65
+            ],
+            "6" => [
+                "0" => 11,
+                "1" => 15,
+                "2" => 19,
+                "3" => 23,
+                "4" => 27,
+                "5" => 31,
+                "6" => 35,
+                "7" => 39,
+                "8" => 43,
+                "9" => 47,
+                "10" => 51,
+                "11" => 55,
+                "12" => 59,
+                "13" => 63,
+                "14" => 67,
+                "15" => 80
+            ]
+        ];
+
+        /**
+         * Values of the ACC main stat[stars][level]
+         */
+        const ACC = [
+            "1" => [
+                "0" => 1,
+                "1" => 2,
+                "2" => 3,
+                "3" => 4,
+                "4" => 5,
+                "5" => 6,
+                "6" => 7,
+                "7" => 8,
+                "8" => 9,
+                "9" => 10,
+                "10" => 11,
+                "11" => 12,
+                "12" => 13,
+                "13" => 14,
+                "14" => 15,
+                "15" => 18
+            ],
+            "2" => [
+                "0" => 2,
+                "1" => 3,
+                "2" => 4,
+                "3" => 5,
+                "4" => 6,
+                "5" => 7,
+                "6" => 8,
+                "7" => 9,
+                "8" => 10,
+                "9" => 11,
+                "10" => 12,
+                "11" => 13,
+                "12" => 14,
+                "13" => 15,
+                "14" => 16,
+                "15" => 19
+            ],
+            "3" => [
+                "0" => 4,
+                "1" => 6,
+                "2" => 8,
+                "3" => 10,
+                "4" => 12,
+                "5" => 14,
+                "6" => 16,
+                "7" => 18,
+                "8" => 20,
+                "9" => 22,
+                "10" => 24,
+                "11" => 26,
+                "12" => 28,
+                "13" => 30,
+                "14" => 32,
+                "15" => 38
+            ],
+            "4" => [
+                "0" => 6,
+                "1" => 8,
+                "2" => 10,
+                "3" => 13,
+                "4" => 15,
+                "5" => 17,
+                "6" => 19,
+                "7" => 21,
+                "8" => 24,
+                "9" => 26,
+                "10" => 28,
+                "11" => 30,
+                "12" => 32,
+                "13" => 35,
+                "14" => 37,
+                "15" => 44
+            ],
+            "5" => [
+                "0" => 9,
+                "1" => 11,
+                "2" => 14,
+                "3" => 16,
+                "4" => 19,
+                "5" => 21,
+                "6" => 23,
+                "7" => 26,
+                "8" => 28,
+                "9" => 31,
+                "10" => 33,
+                "11" => 35,
+                "12" => 38,
+                "13" => 40,
+                "14" => 43,
+                "15" => 51
+            ],
+            "6" => [
+                "0" => 12,
+                "1" => 15,
+                "2" => 18,
+                "3" => 21,
+                "4" => 24,
+                "5" => 27,
+                "6" => 30,
+                "7" => 33,
+                "8" => 36,
+                "9" => 39,
+                "10" => 42,
+                "11" => 45,
+                "12" => 48,
+                "13" => 51,
+                "14" => 54,
+                "15" => 64
+            ]
+        ];
+
+        /**
+         * Values of the RES main stat[stars][level]
+         */
+        const RES = [
+            "1" => [
+                "0" => 1,
+                "1" => 2,
+                "2" => 3,
+                "3" => 4,
+                "4" => 5,
+                "5" => 6,
+                "6" => 7,
+                "7" => 8,
+                "8" => 9,
+                "9" => 10,
+                "10" => 11,
+                "11" => 12,
+                "12" => 13,
+                "13" => 14,
+                "14" => 15,
+                "15" => 18
+            ],
+            "2" => [
+                "0" => 2,
+                "1" => 3,
+                "2" => 4,
+                "3" => 5,
+                "4" => 6,
+                "5" => 7,
+                "6" => 8,
+                "7" => 9,
+                "8" => 10,
+                "9" => 11,
+                "10" => 12,
+                "11" => 13,
+                "12" => 14,
+                "13" => 15,
+                "14" => 16,
+                "15" => 19
+            ],
+            "3" => [
+                "0" => 4,
+                "1" => 6,
+                "2" => 8,
+                "3" => 10,
+                "4" => 12,
+                "5" => 14,
+                "6" => 16,
+                "7" => 18,
+                "8" => 20,
+                "9" => 22,
+                "10" => 24,
+                "11" => 26,
+                "12" => 28,
+                "13" => 30,
+                "14" => 32,
+                "15" => 38
+            ],
+            "4" => [
+                "0" => 6,
+                "1" => 8,
+                "2" => 10,
+                "3" => 13,
+                "4" => 15,
+                "5" => 17,
+                "6" => 19,
+                "7" => 21,
+                "8" => 24,
+                "9" => 26,
+                "10" => 28,
+                "11" => 30,
+                "12" => 32,
+                "13" => 35,
+                "14" => 37,
+                "15" => 44
+            ],
+            "5" => [
+                "0" => 9,
+                "1" => 11,
+                "2" => 14,
+                "3" => 16,
+                "4" => 19,
+                "5" => 21,
+                "6" => 23,
+                "7" => 26,
+                "8" => 28,
+                "9" => 31,
+                "10" => 33,
+                "11" => 35,
+                "12" => 38,
+                "13" => 40,
+                "14" => 43,
+                "15" => 51
+            ],
+            "6" => [
+                "0" => 12,
+                "1" => 15,
+                "2" => 18,
+                "3" => 21,
+                "4" => 24,
+                "5" => 27,
+                "6" => 30,
+                "7" => 33,
+                "8" => 36,
+                "9" => 39,
+                "10" => 42,
+                "11" => 45,
+                "12" => 48,
+                "13" => 51,
+                "14" => 54,
+                "15" => 64
+            ]
+        ];
+    };
+
     /**
      * Artifact types.
      *

+ 4 - 0
application/Controller.php

@@ -87,6 +87,10 @@
                             require_once(__DIR__ . "/API/v2/API_Controller.php");
                             new API_Controller($params);
                             break;
+                        case "v3":
+                            require_once(__DIR__ . "/API/v3/API_Controller.php");
+                            new API_Controller($params);
+                            break;
                     }
                 }
                 return;

BIN
application/sw.sqlite


+ 1 - 1
application/view/runs.php

@@ -322,7 +322,7 @@
 <?php
                                     $date = date_create($run->dtime);
 ?>
-                                    <?=date_format($date, "Y/m/d H:i:s")?>
+                                    <?=date("Y/m/d H:i:s", $run->dtime);//date_format($date, "Y/m/d H:i:s")?>
                                 </td>
                                 <td class='team <?=$class_frontline?>'>
 <?php

+ 39 - 57
swex-plugin/swdb.js

@@ -1,14 +1,3 @@
-/**
- * 
- * List of processed commands:
- * 
- * - HubUserLogin
- * - BattleDungeonResult
- * - BattleScenarioResult
- * - BattleDimensionHoleDungeonResult
- * - BattleTrialTowerResult_v2
- * 
- */
 const dateFormat = require('/usr/local/lib/node_modules/dateformat');
 
 var querystring = require('querystring');
@@ -81,6 +70,8 @@ module.exports = {
     processCommand(command, req, res){
         // Process the command
         var apiCommand = "";
+        var method = "";
+        var params = {'key': config.Config.Plugins[this.pluginName].apiKey};
         var success = null;
         var start = false;
         switch (command){
@@ -97,49 +88,55 @@ module.exports = {
 
             // Profile login
             case 'HubUserLogin':
-                apiCommand = "upload_profile";
-                success = "Profile uploaded sucesfully!"
+                apiCommand = "profile/";
+                method = "PUT";
+                success = "Profile uploaded sucesfully!";
+                params['response'] = JSON.stringify(res);
                 break;
             // Catalog status
             case 'GetUnitCollection':
-                apiCommand = "update_collection";
-                success = "Collection updated sucesfully!"
+                //command = "update_collection";
+                //success = "Collection updated sucesfully!"
                 break;
             // Profile logbook
             case 'GetLobbyWizardLog':
-                apiCommand = "update_logbook";
-                success = "Logbook updated succesfully!";
+                //command = "update_logbook";
+                //success = "Logbook updated succesfully!";
                 break;
             // Battle runs
             case 'BattleDungeonResult_V2':
-                apiCommand = "upload_run_dungeon";
+                apiCommand = "run/";
+                method = "POST";
                 success = "Cairos run logged sucesfully!"
+                params['result_response'] = JSON.stringify(res);
+                params['result_request'] = JSON.stringify(req);
                 break;
             case 'BattleScenarioResult':
-                apiCommand = "upload_run_scenario";
-                success = "Scenario run logged sucesfully!"
+                //command = "log_run_scenario";
+                //success = "Scenario run logged sucesfully!"
+                start = true;
                 break;
             case 'BattleDimensionHoleDungeonResult':
-                apiCommand = "upload_run_dimension";
-                success = "Dimension Hole run logged sucesfully!"
+                //command = "log_run_dh";
+                //success = "Dimension Hole run logged sucesfully!"
                 break;
             case 'BattleTrialTowerResult_v2':
-                apiCommand = "upload_run_toa";
-                success = "TOA run logged sucesfully!"
+                //command = "log_run_toa";
+                //success = "TOA run logged sucesfully!"
                 break;
             case 'BattleRiftDungeonResult':
-                apiCommand = "upload_run_rift";
-                success = "Rift Dungeon run logged sucesfully!"
+                //command = "log_run_rift_dungeon";
+                //success = "Rift Dungeon run logged sucesfully!"
                 start = true;
                 break;
         }
         if (apiCommand != ""){
-            this.command(apiCommand, req, res, start, success);
+            this.command(apiCommand, method, params, success);
         }
     },
 
     /**
-     * Calls an arbitrary command on the SWDB APIv2.
+     * Calls an arbitrary command on the SWDB APIv3.
      *
      * @param command Command nade.
      * @param req The full request data.
@@ -147,50 +144,35 @@ module.exports = {
      * @param start Indicates if event start request and response are needed.
      * @param success Optional. Message to show on success.
      */
-    command(command, req, res, start = false, success = null){
+    command(command, method, params, success = null){
 
         // Override success message if empty
         if (success == null || success == ""){
             sucess = `Command ${command} succesfully executed!`
         }
         this.log('debug', `Starting ${command}...`);
+
         // Generate POST data
-        var post_data;
-        if (start == false){
-            post_data = querystring.stringify({
-                'request' : JSON.stringify(req),
-                'response' : JSON.stringify(res),
-                'key' : config.Config.Plugins[this.pluginName].apiKey
-            });
-        }
-        else{
-            post_data = querystring.stringify({
-                'request' : JSON.stringify(req),
-                'response' : JSON.stringify(res),
-                'start_request' : JSON.stringify(this.eventStartReq),
-                'start_response' : JSON.stringify(this.eventStartRes),
-                'key' : config.Config.Plugins[this.pluginName].apiKey
-            });
-        }
+        var data = querystring.stringify(params);
         // Configure POST requets
         var post_options = {
             host: config.Config.Plugins[this.pluginName].host,
             port: config.Config.Plugins[this.pluginName].port,
-            path: '/API/v2/' + command,
-            method: 'POST',
+            path: '/API/v3/' + command,
+            method: method,
             headers: {
                 'Content-Type': 'application/x-www-form-urlencoded',
-                'Content-Length': Buffer.byteLength(post_data)
+                'Content-Length': Buffer.byteLength(data)
             }
         };
         // Set up the request
-        var post_req = http.request(post_options, (function(post_res) {
-            post_res.setEncoding('utf8');
-            post_res.on('data', (function (body) {
-                switch (post_res.statusCode){
+        var req = http.request(post_options, (function(res) {
+            res.setEncoding('utf8');
+            res.on('data', (function (body) {
+                switch (res.statusCode){
                     case 200:
                     case 201:
-                        this.log('success', `${success} [${post_res.statusCode}]`);
+                        this.log('success', `${success} [${res.statusCode}]`);
                         break;
                     case 400:
                         this.log('error', `[HTTP/1.1 400] There were errors importing the data: ${JSON.stringify(body)}`);
@@ -202,13 +184,13 @@ module.exports = {
                         this.log('error', '[HTTP/1.1 500] The server returned an error.');
                         break;
                     default:
-                        this.log('error', `[HTTP/1.1 ${post_res.statusCode}] Unexpected error.`);
+                        this.log('error', `[HTTP/1.1 ${res.statusCode}] Unexpected error.`);
                 }
             }).bind(this));
         }).bind(this));
         // Perform the request
-        post_req.write(post_data);
-        post_req.end();
+        req.write(data);
+        req.end();
     },
 
     /**