Jelajahi Sumber

Optimizations for the optimizer command.

- Faster.
- Less IO bottleneck.
- Better progress bar.
- Colorized terminal output in human format.
- File output in human format added.
- JSON output to terminal and file added.
- Using getopt_long for the optimizer.
- Outpupt printing code separated from optimizer code.
Iñigo Valentin 4 tahun lalu
induk
melakukan
14c6f9d3c1

+ 1 - 1
Makefile

@@ -22,7 +22,7 @@ endif
 # Files to compile
 # For tests, runeoptimizer.c is not included in the list: is is included from
 # each test file with a redefinition of main.
-FILES=src/cli/runeoptimizer/runeoptimizer.c src/cli/runeoptimizer/*/*.c
+FILES=src/cli/runeoptimizer/runeoptimizer.c src/cli/runeoptimizer/*/*.c src/cli/runeoptimizer/*/*/*.c
 TESTU_FILES=src/cli/test/runeoptimizer_unit_test.c src/cli/runeoptimizer/*/*.c
 TESTI_FILES=src/cli/test/runeoptimizer_integration_test.c src/cli/runeoptimizer/*/*.c
 TESTE_FILES=src/cli/test/runeoptimizer_e2e_test.c src/cli/runeoptimizer/*/*.c

+ 7 - 0
src/cli/runeoptimizer/error/error.h

@@ -220,6 +220,13 @@
  */
 #define ERROR_INPUT_OPTIMIZE_NO_THREADS -121
 
+/**
+ * Optimizer user input error.
+ *
+ * Indicates an unknown value for the output option.
+ */
+#define ERROR_INPUT_OPTIMIZE_PARAMETER_OUTPUT -122
+
 /**
  * Team command user input error.
  *

+ 7 - 0
src/cli/runeoptimizer/help/help.c

@@ -176,6 +176,13 @@ void help(){
       "                                     Unit IDs can be supplied, comma-separated.\n"
       "        -g | --gui                   Formats the output to be consumed by the GUI.\n"
       "        -b | --threads <NUM>         Use <NUM> threads for optimizaton (1-8).\n"
+      "        -z | --output <MODE>         Use MODE to generate optimization report.\n"
+      "                                     Valid modes are:\n"
+      "                                       none: Print the best result on screen, discard the rest.\n"
+      "                                       screen: Print every result on screen on a human readable format.\n"
+      "                                       plain: Save results to a text file, in a human readable format.\n"
+      "                                       json: Save results to a text file, in JSON format.\n"
+      "                                       html: Save results to a interactive HTML file.\n"
 
     );
     return;

File diff ditekan karena terlalu besar
+ 528 - 668
src/cli/runeoptimizer/optimize/optimize.c


+ 51 - 21
src/cli/runeoptimizer/optimize/optimize.h

@@ -345,6 +345,21 @@ typedef struct Result {
     struct Stats stats;
 } Result;
 
+typedef struct Optimizer_Output{
+    // TODO: Document, maybe separate
+    unsigned char verbose;
+    unsigned char quiet;
+    unsigned char debug;
+    unsigned char format;
+    unsigned char output;
+} Optimizer_Output;
+
+typedef struct Optimizer_Options{
+    // TODO: Document, maybe separate
+    unsigned char help;
+    unsigned char threads;
+} Optimizer_Options;
+
 /**
  * Options for the optimizer.
  *
@@ -352,7 +367,7 @@ typedef struct Result {
  * are assigned in {@link optimize_set_default_options}, and the argument values
  * are set in {@link optimize_parse_arguments}.
  */
-typedef struct Optimizer_Options {
+typedef struct Optimizer_Filters {
 
     /**
      * Unit ID.
@@ -572,7 +587,12 @@ typedef struct Optimizer_Options {
      */
     Stat_Ponderation ponderation;
 
-} Optimizer_Options;
+    /**
+     * Output mode for the optimization data.
+     */
+    unsigned char output_mode;
+
+} Optimizer_Filters;
 
 /**
  * Data package used by the optimizer.
@@ -605,42 +625,37 @@ typedef struct Optimizer_Data {
     unsigned int count[RUNE_SLOTS + 1];
 
     /**
-     * The rune in slot 1 that the thread must start making checks.
+     * List of results.
      *
-     * Each thread test a segment of slot 1 agains every combination of slots
-     * 2 to 6, this is where it should start checking.
+     * The thread saves every succesfull combination here.
      */
-    unsigned char start_at;
+    Result results[MAX_RESULTS];
 
     /**
-     * The first rune in slot 1 that the thread must not check.
+     * Number of results.
      *
-     * Each thread test a segment of slot 1 agains every combination of slots
-     * 2 to 6, this is the the position of slot 1 that this thread must not
-     * test.
+     * The total number of results the thread has found.
      */
-    unsigned char end_at;
+    unsigned int total_results;
 
     /**
-     * List of results.
-     *
-     * The thread saves every succesfull combination here.
+     * The options passed to the optimizer.     *
      */
-    Result results[MAX_RESULTS];
+    Optimizer_Options *options;
 
     /**
-     * Number of results.
+     * The filters passed to the optimizer.
      *
-     * The total number of results the thread has found.
+     * The threads also needs to know some of these options.
      */
-    unsigned int total_results;
+    Optimizer_Filters *filters;
 
     /**
-     * The options passed to the optimizer.
+     * The output options passed to the optimizer.
      *
      * The threads also needs to know some of these options.
      */
-    Optimizer_Options options;
+    Optimizer_Output *output;
 
     /**
      * Max testable combinations.
@@ -650,6 +665,13 @@ typedef struct Optimizer_Data {
      */
     unsigned long long max_combinations;
 
+    /**
+     * Tested combinations.
+     *
+     * Number of tested combinations at any given point in time.
+     */
+    unsigned long long tested_combinations;
+
     /**
      * Set count calculated from the options passed to the optimizer.
      *
@@ -662,7 +684,15 @@ typedef struct Optimizer_Data {
      *
      * The unit being optimized, with all the info previously loaded.
      */
-    Unit unit;
+    Unit *unit;
+
+    /**
+     * Optimization start time
+     *
+     * The time the optimization started in ISO format:
+     * YYYY-MM-DDTHH:MM:SS
+     */
+    char start_time[20];
 } Optimizer_Data;
 
 /**

+ 667 - 0
src/cli/runeoptimizer/optimize/output/output.c

@@ -0,0 +1,667 @@
+#include <stdio.h>
+#include <string.h>
+#include "../optimize.h"
+#include "../../db/db.h"
+#include "../../error/error.h"
+#include "output.h"
+
+/**
+ * Generates output in a human readable format.
+ *
+ * @param[in] data Optimizer data. Results, options, unit and rune data must be
+ * already loaded.
+ * @param[in,out] target File to write to. stdout can be passed, to print to the
+ * screen. If it's a file, it must be opened for writing, and in won't be closed
+ * here.
+ * @param[in] color {@link TRUE} to colorize the output, false to not use
+ * colors. It is not recommended to use colors when writing to a file.
+ * @param max Max number of results to print.
+ * @param[in] order {@link ORDER_ASC} to show the worst result first,
+ * {@link ORDER_DESC} to show the best first.
+ * @return The number of results printed.
+ */
+static int print_human(
+  Optimizer_Data *data, FILE *target,
+  unsigned char color, unsigned int max, unsigned char order
+);
+
+/**
+ * Generates output in JSON format.
+ *
+ * @param[in] data Optimizer data. Results, options, unit and rune data must be
+ * already loaded.
+ * @param[in,out] target File to write to. stdout can be passed, to print to the
+ * screen. If it's a file, it must be opened for writing, and in won't be closed
+ * here.
+ * @param max Max number of results to print.
+ * @return The number of results printed.
+ */
+static int print_json(Optimizer_Data *data, FILE *target, unsigned int max);
+
+/**
+ * Generates a filename for the results file.
+ *
+ * Uses the loaded data to generate a filename, with the format
+ * <unit_id>_<unit_name>_<timestamp>.<extension>
+ *
+ * @param[in] data Optimizer data. Results, options, unit and rune data must be
+ * already loaded.
+ * @param[in] ext Filename extension, without the dot.
+ * @param[out] filename Composed filename.
+ * @return Number of characters in the filename.
+ */
+static int generate_filename(Optimizer_Data *data, char *ext, char *filename);
+
+/**
+ * Generates a string with the difference between two stats.
+ *
+ * If the stats are the same, the string will be filled with whitespaces.
+ * If colors are used, the number will have no sign symbol, and the string will
+ * be green if modified is larger than current, and red if it's lower.
+ *
+ * @param[in] current Base value to compare.
+ * @param[in] modified Modified value to compare with base.
+ * @param[in] padding The length for the string. The resultng number will be
+ * padded to the right, and the total length of the string will be this. It can
+ * overflow if padding is shorter than the number length + 1.
+ * @param[in] percent If {@link TRUE}, a percent symbol will be added to the
+ * number. If {@link FALSE}, a whitespace will be added.
+ * @param[out] string The string with the differencec number, with a length of
+ * {@link padding}, padded to the right. If current and modified are the same,
+ * all the string will be filled with whitespaces. If {@link USE_COLOR} is set
+ * to {@link TRUE}, the number will have no sign symbol, and the string will be
+ * green if modified is larger than current, and red if it's lower. If
+ * {@link USE_COLOR} is set to {@link FALSE}, the string will not be colorized,
+ * and it will have a sign symbol, even if it's positive.
+ * @todo: handle color.
+ */
+static int diff_string(
+  unsigned int current, unsigned int modified,
+  int padding, unsigned int percent, unsigned char color, char *string
+);
+
+static int generate_filename(Optimizer_Data *data, char *ext, char *filename){
+    char unit_name[125];
+    int i;
+    for (i = 0; i < strlen(data->unit->name); i ++){
+        if (
+          (data->unit->name[i] >= 'a' && data->unit->name[i] <= 'z')
+          || (data->unit->name[i] >= 'A' && data->unit->name[i] <= 'Z')
+          || (data->unit->name[i] >= '0' && data->unit->name[i] <= '9')
+        ){
+            unit_name[i] = data->unit->name[i];
+        }
+        else{
+            unit_name[i] = '-';
+        }
+    }
+    unit_name[i] = '\0';
+    strcpy(filename, data->unit->id);
+    strcat(filename, "_");
+    strcat(filename, unit_name);
+    strcat(filename, "_");
+    strcat(filename, data->start_time);
+    strcat(filename, ".");
+    strcat(filename, ext);
+    return strlen(filename);
+}
+
+static int diff_string(
+  unsigned int current, unsigned int modified,
+  int padding, unsigned int percent, unsigned char color, char *string
+){
+    int diff = modified - current;
+    int i;
+    if (diff == 0){
+        for (i = 0; i < padding + 1; i ++){
+            string[i] = ' ';
+        }
+
+        string[i] = '\0';
+    }
+    else if (diff > 0){
+        /*sprintf(
+          string, "\033[32m%*d%c\033[39m",
+          padding, diff, (percent == TRUE) ? '%' : ' '
+        );*/
+        sprintf(
+          string, "%s%*d%c%s",
+          (color == TRUE) ? "\033[32m" : "+",
+          (color == TRUE) ? padding : padding - 1,
+          diff, (percent == TRUE) ? '%' : ' ',
+          (color == TRUE) ? "\033[39m" : ""
+        );
+    }
+    else{
+        sprintf(
+          string, "\033[31m%*d%c\033[39m",
+          padding, diff * -1, (percent == TRUE) ? '%' : ' '
+        );
+    }
+    return diff;
+}
+
+extern int output(Optimizer_Data *data){
+    FILE *target;
+    unsigned char format = data->output->format;
+    unsigned char output = data->output->output;
+    int retval = ERROR_INPUT_OPTIMIZE_PARAMETER_OUTPUT;
+    if (format == OUTPUT_HUMAN){
+        if (output == OUTPUT_SCREEN){
+            return print_human(
+              data, stdout, TRUE, MAX_RESULTS_SCREEN_HUMAN, ORDER_ASC
+            );
+        }
+        else if (output == OUTPUT_FILE){
+            char filename[128];
+            generate_filename(data, "txt", filename);
+            FILE * file;
+            file = fopen(filename, "w");
+            retval = print_human(
+              data, file, FALSE, MAX_RESULTS_FILE_HUMAN, ORDER_DESC
+            );
+            fclose(file);
+            return retval;
+        }
+    }
+    else if (format == OUTPUT_JSON){
+        if (output == OUTPUT_SCREEN){
+            return print_json(data, stdout, MAX_RESULTS_SCREEN_JSON);
+        }
+        else if (output == OUTPUT_FILE){
+            char filename[128];
+            generate_filename(data, "json", filename);
+            FILE * file;
+            file = fopen(filename, "w");
+            retval = print_json(data, file, MAX_RESULTS_FILE_JSON);
+            fclose(file);
+            return retval;
+        }
+    }
+    return retval;
+}
+
+extern void output_summary(Optimizer_Data *data){
+    // This is an output example:
+    ////////////////////////////////
+    //
+    // -----------------------------------    Requested rune sets:
+    // | Lushen                          |       RAGE  BLADE
+    // | ID: 7223811472                  |
+    // -----------------------------------    Accepted stats:
+    // | STAT | BASE   | CURR.  | MIN.   |       ATK  CRR  CRD
+    // -----------------------------------
+    // | HP:  |  9225  | 12262  | 10000  |    Considering runes at level 15
+    // | ATK: |   900  |  2482  |  2482  |
+    // | DEF: |   461  |   703  |   703  |    Runes considered by slot:
+    // | SPD: |   103  |   159  |   159  |      1: 27    2: 39    3: 25
+    // | CRR: |    15% |    79% |    79% |      4: 35    5: 24    6: 32
+    // | CRD: |    50% |   188% |   188% |
+    // | RES: |    15% |    35% |    35% |    Total combinations: 707616000
+    // | ACC: |     0% |    11% |    10% |
+    // -----------------------------------
+    //
+    printf(
+      " -----------------------------------------    Requested rune sets:\n"
+    );
+    printf(" | %*s |      ", -37, data->unit->name);
+    for (int i = 0; i < MAX_SETS; i ++){
+        if (data->filters->sets[i] != 0){
+            //printf(" %d ", data->filters->sets[i]);
+            printf(" %s ", SET_NAMES[data->filters->sets[i]]);
+        }
+        else break;
+    }
+    printf("\n");
+    printf(" | ID: %*s |\n", -33, data->unit->id);
+    printf(" -----------------------------------------    Accepted stats:\n");
+    printf(" | STAT | BASE     | CURR.    | MIN.     |      ");
+    for (int i = 0; i < DIFFERENT_STATS; i ++){
+        if (data->filters->stats[i] == TRUE)
+            printf(" %s ", STAT_NAMES[i]);
+        //else break;
+    }
+    printf("\n");
+    printf(" -----------------------------------------\n");
+    printf(
+      " | HP:  | %*u  | %*u  | %*u  |    ",
+      7, data->unit->base_hp, 7, data->unit->current_hp,
+      7, data->filters->min_stats->hp
+    );
+    if (data->filters->level == 0)
+        printf("Using runes at their current level\n");
+    else printf("Considering runes at level %d\n", data->filters->level);
+    printf(
+      " | ATK: | %*u  | %*u  | %*u  |\n",
+      7, data->unit->base_atk, 7, data->unit->current_atk,
+      7, data->filters->min_stats->atk
+    );
+    printf(
+      " | DEF: | %*u  | %*u  | %*u  |    Runes considered by slot:\n",
+      7, data->unit->base_def, 7, data->unit->current_def,
+      7, data->filters->min_stats->def
+    );
+    printf(
+      " | SPD: | %*u  | %*u  | %*u  |      1:%*u    2:%*u    3:%*u\n",
+      7, data->unit->base_spd, 7, data->unit->current_spd,
+      7, data->filters->min_stats->spd,
+      3, data->count[1], 3, data->count[2], 3, data->count[3]
+    );
+    printf(
+      " | CRR: | %*u%% | %*u%% | %*u%% |      4:%*u    5:%*u    6:%*u\n",
+      7, data->unit->base_crr, 7, data->unit->current_crr,
+      7, data->filters->min_stats->crr,
+      3, data->count[4], 3, data->count[5], 3, data->count[6]
+    );
+    printf(
+      " | CRD: | %*u%% | %*u%% | %*u%% |\n",
+      7, data->unit->base_crd, 7, data->unit->current_crd,
+      7, data->filters->min_stats->crd
+    );
+    printf(
+      " | RES: | %*u%% | %*u%% | %*u%% |    Total combinations: %llu\n",
+      7, data->unit->base_res, 7, data->unit->current_res,
+      7, data->filters->min_stats->res, data->max_combinations
+    );
+    printf(
+      " | ACC: | %*u%% | %*u%% | %*u%% |\n",
+      7, data->unit->base_acc, 7, data->unit->current_acc,
+      7, data->filters->min_stats->acc
+    );
+    printf(
+      " | EHP: | %*u  | %*u  | %*u  |\n",
+      7, data->unit->base_ehp, 7, data->unit->current_ehp,
+      7, data->filters->min_stats->ehp
+    );
+    printf(
+      " | DMG: | %*u  | %*u  | %*u  |\n",
+      7, data->unit->base_dmg, 7, data->unit->current_dmg,
+      7, data->filters->min_stats->dmg
+    );
+    printf(" -----------------------------------------\n");
+    return;
+}
+
+static int print_human(
+  Optimizer_Data *data, FILE *target,
+  unsigned char color, unsigned int max, unsigned char order
+){
+    if (data->total_results == 0){
+        fprintf(target, "\n\tNo results found for the selected settings.\n");
+        return 0;
+    }
+    char diff[19];
+    char best_result[30] = "";
+    int total_printed = 0;
+    Unit *unit = data->unit;
+    int i;
+    if (order == ORDER_ASC){
+        i = data->total_results - 1;
+        if (i > max){
+            i = max;
+        }
+    }
+    else{
+        i = 0;
+    }
+
+    while (1){
+        if (i != data->total_results){
+            fprintf(
+              target,
+              "\n==========================================="
+              "==========================================\n"
+            );
+        }
+        Result result = data->results[i];
+        fprintf(target, "--------------------------------------\n");
+        diff_string(unit->current_hp, result.stats.hp, 6, FALSE, color, diff);
+        if (i == 0){
+            if (color == TRUE)
+                strcpy(best_result, "\033[31mBest result!\033[39m");
+            else strcpy(best_result, "Best result!");
+        }
+        fprintf(
+          target,
+          "| HP:  | %*u  | %*u  | %s |      Result #%d %s \n",
+          6, unit->current_hp, 6, result.stats.hp, diff, i + 1, best_result
+        );
+        diff_string(unit->current_atk, result.stats.atk, 6, FALSE, color, diff);
+        fprintf(
+          target,
+          "| ATK: | %*u  | %*u  | %s |\n",
+          6, unit->current_atk, 6, result.stats.atk, diff
+        );
+        diff_string(unit->current_def, result.stats.def, 6, FALSE, color, diff);
+        char rating[30];
+        if (color == TRUE){
+            sprintf(
+              rating, "%s%d%s", (result.rating > 0) ? "\033[32m" : "\033[31m",
+              result.rating, "\033[39m"
+            );
+        }
+        else{
+            sprintf(rating, "%d", result.rating);
+        }
+        fprintf(
+          target,
+          "| DEF: | %*u  | %*u  | %s |      Rating: %s \n",
+          6, unit->current_def, 6, result.stats.def, diff,
+          rating
+        );
+        diff_string(unit->current_spd, result.stats.spd, 6, FALSE, color, diff);
+        fprintf(
+          target,
+          "| SPD: | %*u  | %*u  | %s |\n",
+          6, unit->current_spd, 6, result.stats.spd, diff
+        );
+        diff_string(unit->current_crr, result.stats.crr, 6, FALSE, color, diff);
+        fprintf(
+          target,
+          "| CRR: | %*u  | %*u  | %s |\n",
+          6, unit->current_crr, 6, result.stats.crr, diff
+        );
+        diff_string(unit->current_crd, result.stats.crd, 6, TRUE, color, diff);
+        fprintf(
+          target,
+          "| CRD: | %*u  | %*u  | %s |\n",
+          6, unit->current_crd, 6, result.stats.crd, diff
+        );
+        diff_string(unit->current_res, result.stats.res, 6, TRUE, color, diff);
+        fprintf(
+          target,
+          "| RES: | %*u  | %*u  | %s |\n",
+          6, unit->current_res, 6, result.stats.res, diff
+        );
+        diff_string(unit->current_acc, result.stats.acc, 6, TRUE, color, diff);
+        fprintf(
+          target,
+          "| ACC: | %*u  | %*u  | %s |\n",
+          6, unit->current_acc, 6, result.stats.acc, diff
+        );
+        diff_string(unit->current_ehp, result.stats.ehp, 6, TRUE, color, diff);
+        fprintf(
+          target,
+          "| EHP: | %*u  | %*u  | %s |\n",
+          6, unit->current_ehp, 6, result.stats.ehp, diff
+        );
+        diff_string(unit->current_dmg, result.stats.dmg, 6, TRUE, color, diff);
+        fprintf(
+          target,
+          "| DMG: | %*u  | %*u  | %s |\n",
+          6, unit->current_dmg, 6, result.stats.dmg, diff
+        );
+
+        fprintf(
+          target,
+          "-------------------------------------------"
+          "------------------------------------------\n"
+        );
+        char lines[10][190];
+        for (int l = 0; l < 10; l ++){
+            strcpy(lines[l], "");
+        }
+
+        // Print all the runes in the result
+        int start_pos = 0;
+        for (int r = 0; r < 6; r ++){
+            char rune_id[RUNE_ID_LEN];
+            strcpy(rune_id, result.rune_ids[r]);
+            start_pos = 14 * r;
+            Rune rune;
+            char tmp[64];
+            char tmp2[64];
+            sqlite3_stmt *stmt_rune;
+            char *parameters[1] = {(char *) rune_id};
+            db_query(
+              &stmt_rune,
+              "SELECT "
+              "  unit, type, stars, level, quality, efficiency, max_efficiency "
+              "FROM runes WHERE id = ?",
+              parameters
+            );
+            sqlite3_step(stmt_rune);
+
+            // Row 0: Set name (coloreb by quality) and level
+            int quality = sqlite3_column_int(stmt_rune, 4);
+            if (color == TRUE){
+                switch (quality){
+                    case 1: strcpy(tmp, "\033[30m"); break;
+                    case 2: strcpy(tmp, "\033[32m"); break;
+                    case 3: strcpy(tmp, "\033[36m"); break;
+                    case 4: strcpy(tmp, "\033[35m"); break;
+                    case 5: strcpy(tmp, "\033[33m"); break;
+                    default: strcpy(tmp, "\033[30m");
+                }
+            }
+            else strcpy(tmp, "");
+
+            sprintf(
+              tmp, "%s%-9s", tmp, SET_NAMES[sqlite3_column_int(stmt_rune, 2)]
+            );
+            if (color == TRUE) strcat(tmp, "\033[39m");
+            sprintf(tmp, "%s +%2d", tmp, sqlite3_column_int(stmt_rune, 3));
+            strcat(lines[0], "|");
+            strcat(lines[0], tmp);
+
+            // Row 1, rune ID
+            strcat(lines[1], "| #");
+            strcat(lines[1], rune_id);
+
+            // Row 2, rune location
+            if (strlen(sqlite3_column_text(stmt_rune, 0)) == 0){
+                strcat(lines[2], "| In inventory");
+            }
+            else{
+                if (strcmp(sqlite3_column_text(stmt_rune, 0), unit->id) == 0){
+                    if (color == TRUE)
+                        strcat(lines[2], "| \033[37mEquiped\033[39m     ");
+                    else strcat(lines[2], "| Equiped     ");
+                }
+                else{
+                    // Get unit name
+                    sqlite3_stmt *stmt_unit;
+                    char *parameters_unit[1] = {
+                      (char *) sqlite3_column_text(stmt_rune, 0)
+                    };
+                    db_query(
+                      &stmt_unit,
+                      "SELECT name FROM units WHERE id = ?", parameters_unit
+                    );
+                    if (SQLITE_ROW == sqlite3_step(stmt_unit)){
+                        strcpy(tmp, "             ");
+                        strncpy(tmp, sqlite3_column_text(stmt_unit, 0), 13);
+                    }
+                    else{
+                        strcpy(tmp, " #           ");
+                        strncpy(tmp + 2, sqlite3_column_text(stmt_rune, 0), 11);
+                    }
+                    sqlite3_finalize(stmt_unit);
+                    while (strlen(tmp) < 13){
+                        strcat(tmp, " ");
+                    }
+                    strcat(lines[2], "|");
+                    strcat(lines[2], tmp);
+                }
+            }
+
+            // Get rune stats
+            sqlite3_stmt *stmt_stat;
+            db_query(
+              &stmt_stat,
+              "SELECT "
+              "  slot, stat, value, grind, enchant "
+              "FROM rune_stats WHERE rune = ? ORDER BY slot",
+              parameters
+            );
+            char has_innate = FALSE;
+            char total_subs = 0;
+            char percentile = FALSE;
+            while (SQLITE_ROW == sqlite3_step(stmt_stat)){
+                int slot = sqlite3_column_int(stmt_stat, 0);
+                int stat = sqlite3_column_int(stmt_stat, 1);
+                if (
+                  stat == 2 || stat == 4 || stat == 6 || stat == 8
+                  || stat == 9 || stat == 10 || stat == 11
+                ){
+                    percentile = TRUE;
+                }
+                else{
+                    percentile = FALSE;
+                }
+                if (slot == 0){
+                    has_innate = TRUE;
+                }
+                else if (slot > total_subs){
+                    total_subs = slot;
+                }
+                // Stat name and value.
+                sprintf(
+                  tmp, "%3s%4d%c",
+                  STAT_NAMES_UNSIGNED[stat], sqlite3_column_int(stmt_stat, 2),
+                  (percentile == TRUE) ? 'X': ' '
+                );
+                if (slot == 0){
+                    strcpy(tmp2, tmp);
+                    strcpy(tmp, (color == TRUE) ? "\033[37m" : "");
+                    strcat(tmp, tmp2);
+                    strcat(tmp, (color == TRUE) ? "\033[39m" : "");
+                }
+                // If enchanted, color stat name and value.
+                if (sqlite3_column_int(stmt_stat, 4) == 1){
+                    strcpy(tmp2, tmp);
+                    strcpy(tmp, (color == TRUE) ? "\033[33m" : "");
+                    strcat(tmp, tmp2);
+                    strcat(tmp, (color == TRUE) ? "\033[39m" : "");
+                }
+                // Grind, if any, colored
+                if (sqlite3_column_int(stmt_stat, 3) > 0){
+                    strcat(tmp, (color == TRUE) ? "\033[33m+" : "+");
+                    strcpy(tmp2, "");
+                    sprintf(
+                      tmp2, "%d%c",sqlite3_column_int(stmt_stat, 3),
+                      (percentile == TRUE) ? 'X': ' '
+                    );
+                    while (strlen(tmp2) < 4) strcat(tmp2, " ");
+                    strcat(tmp, tmp2);
+                    if (color == TRUE) strcat(tmp, "\033[39m");
+                }
+                else{
+                    strcat(tmp, "     ");
+                }
+                strcat(lines[slot + 4], "|");
+                strcat(lines[slot + 4], tmp);
+            }
+            sqlite3_finalize(stmt_stat);
+            // It the rune has no innate stat, add empty space.
+            if (has_innate == FALSE){
+                strcat(lines[4], "|             ");
+            }
+            // Add empty space for each subsstat the runes doeesn't have.
+            for (; total_subs < 4; total_subs ++){
+                strcat(lines[5 + total_subs], "|             ");
+            }
+            // Row 9: Efficiency
+            sprintf(
+              tmp, "|E: %2.1f/%2.1fX", sqlite3_column_double(stmt_rune, 5),
+              sqlite3_column_double(stmt_rune, 6)
+            );
+            strcat(lines[9], tmp);
+
+            sqlite3_finalize(stmt_rune);
+        }
+        for (int l = 3; l < 10; l ++){
+            for (int x = 0; x < strlen(lines[l]); x ++){
+                if (lines[l][x] == 'X'){
+                    lines[l][x] = '%';
+                }
+            }
+        }
+
+        for (int l = 0; l < 10; l ++){
+            strcat(lines[l], "|\n");
+            // Use puts to keet the percent signs.
+            fputs(lines[l], target);
+        }
+        fprintf(
+          target,
+          "-------------------------------------------"
+          "------------------------------------------\n"
+        );
+        total_printed ++;
+        // Break conditions
+        if (order == ORDER_ASC){
+            if (0 == i){
+                break;
+            }
+            i --;
+        }
+        else{
+            if (i == data->total_results - 1 || i == max - 1){
+                break;
+            }
+            i ++;
+        }
+    }
+    return total_printed;
+}
+
+static int print_json(Optimizer_Data *data, FILE *target, unsigned int max){
+    char tmp[300];
+    strcpy(tmp, "");
+    char json[300];
+    fprintf(target, "{\"result_count\":%d,", data->total_results);
+    fprintf(target, "\"rune_level\":%d,", data->filters->level);
+    fprintf(target, "\"results\":[");
+    int total_printed = 0;
+    if (data->total_results == 0){
+        fprintf(target, "{\"result_count\":0,\"results\":[]}\n");
+        return 0;
+    }
+    for (int i = 0; i < data->total_results && i < max; i++){
+        sprintf(tmp, "{\"id\":%d,\"rating\":%d,", i, data->results[i].rating);
+        strcpy(json, tmp);
+        sprintf(tmp, "\"hp\":%u,", data->results[i].stats.hp);
+        strcat(json, tmp);
+        sprintf(tmp, "\"atk\":%u,", data->results[i].stats.atk);
+        strcat(json, tmp);
+        sprintf(tmp, "\"dfc\":%u,", data->results[i].stats.def);
+        strcat(json, tmp);
+        sprintf(tmp, "\"spd\":%u,", data->results[i].stats.spd);
+        strcat(json, tmp);
+        sprintf(tmp, "\"crr\":%u,", data->results[i].stats.crr);
+        strcat(json, tmp);
+        sprintf(tmp, "\"crd\":%u,", data->results[i].stats.crd);
+        strcat(json, tmp);
+        sprintf(tmp, "\"res\":%u,", data->results[i].stats.res);
+        strcat(json, tmp);
+        sprintf(tmp, "\"acc\":%u,", data->results[i].stats.acc);
+        strcat(json, tmp);
+        sprintf(tmp, "\"ehp\":%u,", data->results[i].stats.ehp);
+        strcat(json, tmp);
+        sprintf(tmp, "\"dmg\":%u,", data->results[i].stats.dmg);
+        strcat(json, tmp);
+        strcat(json, "\"runes\":[");
+        //printf("    A %s", json);
+        for (int j = 0; j < RUNE_SLOTS; j++){
+            //printf("    RUNE LOOP %d", j);
+            sprintf(tmp, "\"%s\",", data->results[i].rune_ids[j]);
+            strcat(json, tmp);
+        }
+        //printf("    B %s", json);
+        sprintf(tmp, "\"%s\"", data->results[i].rune_ids[5]);
+        strcat(json, tmp);
+        strcat(json, "]},");
+
+        // If last result, remove last comma
+        if (i == data->total_results - 1 || i == max - 1){
+            json[strlen(json) - 1] = 0;
+        }
+        total_printed ++;
+        fprintf(target, json);
+    }
+
+    // End
+    fprintf(target, "]}\n");
+    return total_printed;
+}

+ 108 - 0
src/cli/runeoptimizer/optimize/output/output.h

@@ -0,0 +1,108 @@
+#include "../optimize.h"
+
+/**
+ * Maximum resuts of results to print to console in human format.
+ */
+#define MAX_RESULTS_SCREEN_HUMAN 10
+
+/**
+ * Maximum resuts of results to print to console in JSON format.
+ */
+#define MAX_RESULTS_SCREEN_JSON 100
+
+/**
+ * Maximum resuts of results to print to a file in human format.
+ */
+#define MAX_RESULTS_FILE_HUMAN 50
+
+/**
+ * Maximum resuts of results to print to a file in JSON format.
+ */
+#define MAX_RESULTS_FILE_JSON 1000
+
+/**
+ * Flag to colorize human console output.
+ */
+#define USE_COLORS TRUE
+
+/**
+ * Flag for console output.
+ */
+#define OUTPUT_SCREEN 0
+
+/**
+ * Flag for file output.
+ */
+#define OUTPUT_FILE 1
+
+/**
+ * Flag for human format output.
+ */
+#define OUTPUT_HUMAN 0
+
+/**
+ * Flag for JSON format output.
+ */
+#define OUTPUT_JSON 1
+
+/**
+ * Flag for sort result by rating in ascending order (worst results first).
+ */
+#define ORDER_ASC 0
+
+/**
+ * Flag for sort result by rating in descending order (best results first).
+ */
+#define ORDER_DESC 1
+
+/**
+ * Print a nice summary to the screen.
+ *
+ * Used before starting the long process of optimization, displays the selected
+ * unit, with it's base and current stats, and a list of the options passed to
+ * the optimizer.
+ *
+ * It is exactly 17 lines high and up to 80 characters wide.
+ *
+ * @param[in] data Optimizer data. Results can be unitialized, but options,
+ * unit and rune data must be already loaded.
+ */
+extern void output_summary(Optimizer_Data *data);
+
+/**
+ * Displays all the results.
+ *
+ * It determines the format and the target from the options.
+ *
+ * @param[in] data Optimizer data. Results, options, unit and rune data must be
+ * already loaded.
+ * @return The number of results printed.
+ */
+extern int output(Optimizer_Data *data);
+
+/**
+ * Presents all the results on screen in a human readable format.
+ *
+ * Only the last {@link MAX_SCREEN_RESULTS} will be printed. If
+ * {@link USE_COLORS} is set, the output will be colorized for clarity. The
+ * output is 43 lines high per result and 86 characters wide.
+ *
+ * @param[in] data Optimizer data. Results, options, unit and rune data must be
+ * already loaded.
+ * @return The number of results printed.
+ */
+extern int output_screen(Optimizer_Data *data);
+
+/**
+ * Generates a JSON object with the results and prints them to the screen.
+ *
+ * Only the last {@link MAX_JSON_RESULTS} will be printed.
+ *
+ * @param[in] data Optimizer data. Results, options, unit and rune data must be
+ * already loaded.
+ * @return The number of results printed.
+ */
+extern int output_json(Optimizer_Data *data);
+
+
+

+ 7 - 0
src/cli/runeoptimizer/runeoptimizer.c

@@ -191,6 +191,13 @@ const char STAT_NAMES_PRINTABLE[DIFFERENT_STATS][12] = {
   "NULL",
   "SPD %4d  ", "CRR %4d%% ", "CRD %4d%% ", "RES %4d%% ", "ACC %4d%% " // 8-12
 };
+
+const char STAT_NAMES_UNSIGNED[DIFFERENT_STATS][4] = {
+  "NUL",
+  "HP ", "HP ", "ATK", "ATK", "DEF", "DEF", // 1-6
+  "NUL",
+  "SPD", "CRR", "CRD", "RES", "ACC" // 8-12
+};
 const int STAT_ROLL_MAX[DIFFERENT_STATS][RUNE_MAX_STARS + 1] = {
   {-1, -1, -1, -1, -1, -1, -1}, // NULL (unused)
   {-1, 60, 105, 165, 225, 300, 375}, // HP

+ 10 - 0
src/cli/runeoptimizer/runeoptimizer.h

@@ -763,6 +763,16 @@ extern const char QUALITY_NAMES[DIFFERENT_QUALITIES][9];
  */
 extern const char STAT_NAMES_PRINTABLE[DIFFERENT_STATS][12];
 
+/**
+ * Names for rune sets.
+ *
+ * Thy don't indicate if it's for a flat or a percentile stat.
+ *
+ * Indexed by Com2Us ID.Watch out for indexes 0, 9 and 12, they are undefined in
+ * the game.
+ */
+extern const char STAT_NAMES_UNSIGNED[DIFFERENT_STATS][4];
+
 /**
  * Max roll values for each stat.
  *

+ 4 - 4
src/cli/runeoptimizer/unit/unit.c

@@ -263,8 +263,8 @@ static int unit_list(char *search){
             // Power up level
             strncpy(
               rune_lines[top_line_to_add + 2] + position_to_add + 7,
-              " +      ",
-              9
+              " +",
+              2
             );
             strncpy(
               rune_lines[top_line_to_add + 2] + position_to_add + 9,
@@ -278,7 +278,7 @@ static int unit_list(char *search){
               1
             );
             strncpy(
-              rune_lines[top_line_to_add + 2] + position_to_add + 14, "*", 2
+              rune_lines[top_line_to_add + 2] + position_to_add + 14, "*", 1
             );
             // Quality
             strncpy(
@@ -328,7 +328,7 @@ static int unit_list(char *search){
                         top_line_to_add + 5 + sqlite3_column_int(stmt_stats, 0)
                       ] + position_to_add + 9,
                       "*",
-                      2
+                      1
                     );
                 }
                 // Grinded?

+ 1 - 1
src/gui/runeoptimizer_gui/gui/PanelOptimizer.py

@@ -587,7 +587,7 @@ class PanelOptimizer(wx.Panel):
             self._rune_label_list[i].SetLabel("")
         i = 0
         current_equiped_stats = [0, 0, 0]
-        current_equiped_set_list = [0] * 23;
+        current_equiped_set_list = [0] * 24;
         for rune in self._selected_unit.runes:
             if rune is None:
                 continue

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini