aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Makefile40
-rw-r--r--README.md17
-rw-r--r--analyzer/analyzer.c72
-rw-r--r--analyzer/analyzer.h7
-rw-r--r--analyzer/commands.c98
-rw-r--r--analyzer/commands.h30
-rw-r--r--analyzer/parser/fight_results/drif.h52
-rw-r--r--analyzer/parser/fight_results/entity.h67
-rw-r--r--analyzer/parser/fight_results/fight_results.c260
-rw-r--r--analyzer/parser/fight_results/fight_results.h60
-rw-r--r--analyzer/parser/fight_results/gear.h45
-rw-r--r--analyzer/parser/fight_results/gear_info.h44
-rw-r--r--analyzer/parser/fight_results/orb.h33
-rw-r--r--analyzer/parser/find_parse_functions.h6
-rw-r--r--analyzer/parser/parser.c64
-rw-r--r--analyzer/parser/parser.h23
-rw-r--r--analyzer/parser/parser_defs.h42
-rw-r--r--analyzer/parser/structs.c51
-rw-r--r--analyzer/parser/structs.h33
-rw-r--r--analyzer/render.c162
-rw-r--r--analyzer/render.h55
-rw-r--r--analyzer/structures/hashmap.c116
-rw-r--r--analyzer/structures/hashmap.h81
-rw-r--r--analyzer/structures/heap.c113
-rw-r--r--analyzer/structures/heap.h60
-rw-r--r--analyzer/utils/fs.c13
-rw-r--r--analyzer/utils/fs.h11
-rw-r--r--analyzer/utils/memory.c62
-rw-r--r--analyzer/utils/memory.h46
-rw-r--r--analyzer/utils/roman.c15
-rw-r--r--analyzer/utils/roman.h29
-rw-r--r--analyzer/utils/tcp.c177
-rw-r--r--analyzer/utils/tcp.h69
-rw-r--r--analyzer/utils/tcp_seq_math.c41
-rw-r--r--analyzer/utils/tcp_seq_math.h28
-rw-r--r--analyzer/utils/time.c2
-rw-r--r--csv/artifact_sizes.csv5
-rw-r--r--csv/items.csv6
-rw-r--r--csv/orbs.csv3
-rw-r--r--csv/saturations.csv8
-rw-r--r--csv_table_gen.c125
-rw-r--r--main.c63
43 files changed, 2247 insertions, 88 deletions
diff --git a/.gitignore b/.gitignore
index dc2bf7f..802b083 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
compile_commands.json
sample_data/
analyzer/headers/server.h
+analyzer/csv/
diff --git a/Makefile b/Makefile
index a85df64..cc5cb87 100644
--- a/Makefile
+++ b/Makefile
@@ -1,16 +1,44 @@
+CC=gcc
+CFLAGS=-Wall -iquote ./analyzer -O2 -flto -s
+
global: compile
sudo mv branal /usr/bin
local: compile
mv branal ${HOME}/.local/bin
-compile: utils capture
- gcc *.o main.c -o branal
+debug: CFLAGS=-iquote ./analyzer -Og -g
+debug: global
+
+compile: csv_table_gen main_files structures utils parser
+ $(CC) $(CFLAGS) -lpcap *.o main.c -o branal
rm *.o
+csv_table_gen:
+ $(CC) $(CFLAGS) csv_table_gen.c -o csv_table_gen
+ ./csv_table_gen
+ rm csv_table_gen
+ $(CC) $(CFLAGS) -c analyzer/csv/*.c
+
+main_files:
+ $(CC) $(CFLAGS) -c analyzer/commands.c
+ $(CC) $(CFLAGS) -c analyzer/capture.c
+ $(CC) $(CFLAGS) -c analyzer/analyzer.c
+ $(CC) $(CFLAGS) -c analyzer/render.c
+
+structures:
+ $(CC) $(CFLAGS) -c analyzer/structures/hashmap.c
+ $(CC) $(CFLAGS) -c analyzer/structures/heap.c
+
utils:
- gcc -c analyzer/utils/fs.c
- gcc -c analyzer/utils/time.c
+ $(CC) $(CFLAGS) -c analyzer/utils/fs.c
+ $(CC) $(CFLAGS) -c analyzer/utils/time.c
+ $(CC) $(CFLAGS) -c analyzer/utils/tcp.c
+ $(CC) $(CFLAGS) -c analyzer/utils/tcp_seq_math.c
+ $(CC) $(CFLAGS) -c analyzer/utils/roman.c
+ $(CC) $(CFLAGS) -c analyzer/utils/memory.c
-capture:
- gcc -c analyzer/capture.c
+parser:
+ $(CC) $(CFLAGS) -c analyzer/parser/parser.c
+ $(CC) $(CFLAGS) -c analyzer/parser/structs.c
+ $(CC) $(CFLAGS) -c analyzer/parser/fight_results/fight_results.c
diff --git a/README.md b/README.md
index 780548b..c10b3de 100644
--- a/README.md
+++ b/README.md
@@ -6,10 +6,11 @@ Unless you value FOSS or have troubles setting up BrokenHUD on your linux system
## Features
- Capturing packets sent by the game and saving them to a file for later use
-
-### Planned features
- Analyzer for the `.pcap` files containing captured packets:
- List of fight results in the given file with rewards gained for each player
+
+### Planned features
+- File analysis:
- Total gained rewards in all fights in the given file
- Live analyzer parsing packets in real time:
- Results of the last fight
@@ -80,20 +81,20 @@ branal (l | live)
```
It will open the live analyzer in your terminal.
-### File analyzer (NOT IMPLEMENTED)
-To analize a `.pcap` file, use:
+### File analyzer
+To analize `.pcap` files, use:
```
-branal (f | file) <file>
+branal (f | file) <file...>
```
-It will perform analysis of the specified `<file>`.
+It will perform analysis of the specified file(s).
### Capturing packets
To just capture packets from the server, use:
```
branal (c | capture) [file]
```
-It will start capturing packets to the specified `[file]`.
-If `[file]` is not specified, it will capture them to the default location (`~/.local/share/branal/captures`).
+It will start capturing packets to the specified file.
+If file is not specified, it will capture them to the default location (`~/.local/share/branal/captures`).
### Help
You can display help using:
diff --git a/analyzer/analyzer.c b/analyzer/analyzer.c
new file mode 100644
index 0000000..aa4a645
--- /dev/null
+++ b/analyzer/analyzer.c
@@ -0,0 +1,72 @@
+#include "analyzer.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "parser/parser.h"
+#include "render.h"
+#include "utils/tcp.h"
+
+// loop through packets received from pcap_handle and analyze the TCP stream
+void analyzer_loop(
+ TCPStream *stream, pcap_t *pcap_handle, void *state, void (*parsed_data_handler)(void *state, ParsedData *parsed_data, size_t n_parsed_data)
+) {
+ // read packets from handle
+ struct pcap_pkthdr *packet_data;
+ const byte_t *packet;
+ while (pcap_next_ex(pcap_handle, &packet_data, &packet) == 1) {
+ // get new segment
+ TCPSegment segment = get_segment_from_packet(packet, packet_data);
+
+ // handle the new segment if it's correct one and has data in it (ignore SYN)
+ if (segment.len > 0 && handle_segment(stream, segment)) {
+ // if it was added to the stream - parse it
+ size_t parsed_len;
+ size_t n_parsed_data;
+ ParsedData *parsed_data = parse_stream(stream, &parsed_len, &n_parsed_data);
+
+ // if some bytes were parsed - remove them from the stream
+ if (parsed_len > 0)
+ remove_parsed_data(stream, parsed_len);
+
+ // if there's parsed data - call the parsed data handler
+ if (n_parsed_data > 0)
+ parsed_data_handler(state, parsed_data, n_parsed_data);
+
+ // free the entire parsed data memory
+ free_parsed_data(parsed_data, n_parsed_data);
+ }
+ }
+}
+
+// file analyzer parsed data handler
+void file_parsed_data_handler(void *state, ParsedData *parsed_data, size_t n_parsed_data) {
+ int *fights_count = state;
+ for (size_t i = 0; i < n_parsed_data; i++) {
+ switch (parsed_data[i].data_type) {
+ case TYPE_FIGHT_RESULTS:
+ printf("\nFIGHT %d:\n", ++*fights_count);
+ render_fight_results(*(FightResults *)parsed_data[i].data);
+ break;
+ }
+ }
+}
+
+void analyze_file(char *path) {
+ // open the file
+ char errbuf[PCAP_ERRBUF_SIZE];
+ pcap_t *handle = pcap_open_offline(path, errbuf);
+
+ // handle errors
+ if (handle == NULL) {
+ fprintf(stderr, "Couldn't open the specified file:\n%s\n", errbuf);
+ return;
+ }
+
+ // analyze the file
+ TCPStream stream = new_stream();
+ int state = 0;
+ analyzer_loop(&stream, handle, &state, file_parsed_data_handler);
+ destroy_stream(stream);
+ pcap_close(handle);
+}
diff --git a/analyzer/analyzer.h b/analyzer/analyzer.h
new file mode 100644
index 0000000..1b8d2f9
--- /dev/null
+++ b/analyzer/analyzer.h
@@ -0,0 +1,7 @@
+// analyzer.h
+// functions for analyzing the packets received from the game server
+
+// analyze file
+// params:
+// - path - path to the file to analyze
+extern void analyze_file(char *path);
diff --git a/analyzer/commands.c b/analyzer/commands.c
new file mode 100644
index 0000000..912bdf0
--- /dev/null
+++ b/analyzer/commands.c
@@ -0,0 +1,98 @@
+#include "commands.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#include "analyzer.h"
+#include "capture.h"
+#include "utils/memory.h"
+
+#define PROGRAM_NAME "branal"
+#define INDENT " "
+#define MAX_COMMAND_LENGTH 100
+
+// calculate the longest command length to align explenations
+int longest_command_length() {
+ int longest_len = 0;
+ for (size_t i = 0; i < commands_count; i++) {
+ Command cmd = commands[i];
+ int len = strlen(PROGRAM_NAME);
+ len += strlen(" ( | ) ");
+ len += strlen(cmd.short_name) + strlen(cmd.name);
+ len += strlen(cmd.args_str) == 0 ? 0 : strlen(cmd.args_str) + 1;
+ if (len > longest_len)
+ longest_len = len;
+ }
+ return longest_len;
+}
+
+// help command handler
+int print_help(int argc, char *argv[]) {
+ int len = longest_command_length();
+
+ printf("A bare-bones analyzer for the game Broken Ranks\n");
+ printf("\nUsage:\n");
+ printf(INDENT "%-*s- same as below\n", len, PROGRAM_NAME);
+
+ for (size_t i = 0; i < commands_count; i++) {
+ Command cmd = commands[i];
+
+ char buffer[MAX_COMMAND_LENGTH];
+ sprintf(buffer, PROGRAM_NAME " (%s | %s) ", cmd.short_name, cmd.name);
+ if (strlen(cmd.args_str))
+ strcat(buffer, cmd.args_str);
+
+ printf(INDENT "%-*s- %s\n", len, buffer, cmd.explanation[0]);
+ for (size_t j = 1; j < cmd.explanation_lines; j++)
+ printf(INDENT "%*s%s\n", len + 2, "", cmd.explanation[j]);
+ }
+
+ return 0;
+}
+
+// live analyzer command handler
+int live_anal(int argc, char *argv[]) {
+ // TODO: implement
+ printf("Not implemented\n");
+ return 1;
+}
+
+// capturing mode command handler
+int capture(int argc, char *argv[]) {
+ capturing_mode(argc >= 3 ? argv[2] : NULL);
+ return 0;
+}
+
+// file analysis command handler
+int file_anal(int argc, char *argv[]) {
+ if (argc < 3) {
+ printf("You need to provide a file (or files) to analyze");
+ return 1;
+ }
+
+ for (size_t i = 2; i < argc; i++) {
+ printf("FILE: %s\n", argv[i]);
+ analyze_file(argv[i]);
+ printf("\n");
+ }
+
+ return 0;
+}
+
+const Command help_command = {"help", "h", "", {"display this help"}, 1, print_help};
+const Command commands[] = {
+ {"live", "l", "", {"start live analysis"}, 1, live_anal},
+ {"capture", "c", "[file]", {"start capturing packers to file", "If file isn't provided - captures to: $HOME/.local/share/branal/captures"}, 2, capture},
+ {"file", "f", "<file...>", {"analyze the specified file(s)"}, 1, file_anal},
+ help_command
+};
+const size_t commands_count = ARR_LEN(commands);
+
+int is_command(Command command, char *str) {
+ return !strcmp(command.short_name, str) || !strcmp(command.name, str);
+}
+
+void command_not_recognized() {
+ printf("Command not recognized\n");
+ printf("Use \"%s (%s | %s)\" for help\n", PROGRAM_NAME, help_command.short_name, help_command.name);
+}
diff --git a/analyzer/commands.h b/analyzer/commands.h
new file mode 100644
index 0000000..f53a140
--- /dev/null
+++ b/analyzer/commands.h
@@ -0,0 +1,30 @@
+// commands.h
+// commands to use in the program
+
+#include <stddef.h>
+
+#define MAX_EXPLANATION_LINES 3
+
+typedef struct {
+ char *name;
+ char *short_name;
+ char *args_str;
+ char *explanation[MAX_EXPLANATION_LINES];
+ size_t explanation_lines;
+ int (*handler)(int argc, char *argv[]);
+} Command;
+
+// all commands of the program
+extern const Command commands[];
+extern const size_t commands_count;
+
+// check if the given command is represented by the given string
+// params:
+// - command - potentailly represented command
+// - str - potentially representing string
+// returns:
+// 1 if command is represneted by string, 0 if not
+extern int is_command(Command command, char *str);
+
+// print information about help when program didn't recognize command
+extern void command_not_recognized();
diff --git a/analyzer/parser/fight_results/drif.h b/analyzer/parser/fight_results/drif.h
new file mode 100644
index 0000000..1f8b3e2
--- /dev/null
+++ b/analyzer/parser/fight_results/drif.h
@@ -0,0 +1,52 @@
+// parser/fight_results/drif.h
+// Drif generic struct data
+
+#include "parser/structs.h"
+
+// Drif structure:
+// field (SEPARATOR_DRIF_FIELD field)*
+// fields are defined in order in the Drif struct
+#define SEPARATOR_DRIF "???" // TODO: change it to correct one which idk at this moment
+#define SEPARATOR_DRIF_FIELD ","
+
+typedef struct {
+ char *field1; // just says "drif"
+ int field2;
+ int size; // 1 - sub, 2 - bi, 3 - magni, 4 - arcy
+ char *field4;
+ int field5;
+ int id; // id of orb type
+ float value1; // value1, value2 (in %) - one is start value, the other is value per lvl (both are the same)
+ float value2; // value1, value2 (in %) - one is start value, the other is value per lvl (both are the same)
+ int field9;
+ int field10;
+ char *field11;
+ char *size_str; // sub - small, medium - bi, big - magni, ??? - arcy
+ int field13;
+ int power; // how much space it takes
+ int field15;
+ int field16;
+ char *name; // <size>drif <name>
+ char *field18;
+} Drif;
+
+static const StructField drif_fields[] = {
+ {STRING, offsetof(Drif, field1)},
+ {INT, offsetof(Drif, field2)},
+ {INT, offsetof(Drif, size)},
+ {STRING, offsetof(Drif, field4)},
+ {INT, offsetof(Drif, field5)},
+ {INT, offsetof(Drif, id)},
+ {FLOAT, offsetof(Drif, value1)},
+ {FLOAT, offsetof(Drif, value2)},
+ {INT, offsetof(Drif, field9)},
+ {INT, offsetof(Drif, field10)},
+ {STRING, offsetof(Drif, field11)},
+ {STRING, offsetof(Drif, size_str)},
+ {INT, offsetof(Drif, field13)},
+ {INT, offsetof(Drif, power)},
+ {INT, offsetof(Drif, field15)},
+ {INT, offsetof(Drif, field16)},
+ {STRING, offsetof(Drif, name)},
+ {STRING, offsetof(Drif, field18)},
+};
diff --git a/analyzer/parser/fight_results/entity.h b/analyzer/parser/fight_results/entity.h
new file mode 100644
index 0000000..8e082ec
--- /dev/null
+++ b/analyzer/parser/fight_results/entity.h
@@ -0,0 +1,67 @@
+// parser/fight_results/entity.h
+// EntityResults generic struct data
+
+#include "parser/structs.h"
+
+// EntityResults structure:
+// field (SEPARATOR_ENTITY_FIELD field)*
+// fields are defined in order in the EntityResults struct
+#define SEPARATOR_ENTITY "[--]"
+#define SEPARATOR_ENTITY_FIELD "&"
+
+// values of field "team" in EntityResults
+#define TEAM_FRIEND 1
+#define TEAM_ENEMY 2
+
+typedef struct {
+ int team; // friend or enemy
+ char *name;
+ int exp;
+ float field4;
+ int gold;
+ float field6;
+ char *field7;
+ char *gear; // rares, sets, syngs, orbs
+ int field9;
+ char *items; // regular items
+ int field11;
+ int field12;
+ float field13;
+ float field14; // some multiplier most likely exp/gold/psycho/something else idk
+ float field15;
+ int level;
+ double field17;
+ float field18;
+ int field19;
+ int field20;
+ float field21;
+ float field22;
+ int field23;
+ int field24;
+ int psycho;
+ char *drifs;
+ int splinters;
+ int field28;
+ char *field29;
+ int field30;
+ int field31;
+ int field32;
+ int field33;
+ char *field34;
+ char *saturation;
+} EntityResults;
+
+static const StructField entity_fields[] = {
+ {INT, offsetof(EntityResults, team)}, {STRING, offsetof(EntityResults, name)}, {INT, offsetof(EntityResults, exp)},
+ {FLOAT, offsetof(EntityResults, field4)}, {INT, offsetof(EntityResults, gold)}, {FLOAT, offsetof(EntityResults, field6)},
+ {STRING, offsetof(EntityResults, field7)}, {STRING, offsetof(EntityResults, gear)}, {INT, offsetof(EntityResults, field9)},
+ {STRING, offsetof(EntityResults, items)}, {INT, offsetof(EntityResults, field11)}, {INT, offsetof(EntityResults, field12)},
+ {FLOAT, offsetof(EntityResults, field13)}, {FLOAT, offsetof(EntityResults, field14)}, {FLOAT, offsetof(EntityResults, field15)},
+ {INT, offsetof(EntityResults, level)}, {DOUBLE, offsetof(EntityResults, field17)}, {FLOAT, offsetof(EntityResults, field18)},
+ {INT, offsetof(EntityResults, field19)}, {INT, offsetof(EntityResults, field20)}, {FLOAT, offsetof(EntityResults, field21)},
+ {FLOAT, offsetof(EntityResults, field22)}, {INT, offsetof(EntityResults, field23)}, {INT, offsetof(EntityResults, field24)},
+ {INT, offsetof(EntityResults, psycho)}, {STRING, offsetof(EntityResults, drifs)}, {INT, offsetof(EntityResults, splinters)},
+ {INT, offsetof(EntityResults, field28)}, {STRING, offsetof(EntityResults, field29)}, {INT, offsetof(EntityResults, field30)},
+ {INT, offsetof(EntityResults, field31)}, {INT, offsetof(EntityResults, field32)}, {INT, offsetof(EntityResults, field33)},
+ {STRING, offsetof(EntityResults, field34)}, {STRING, offsetof(EntityResults, saturation)},
+};
diff --git a/analyzer/parser/fight_results/fight_results.c b/analyzer/parser/fight_results/fight_results.c
new file mode 100644
index 0000000..efc4b36
--- /dev/null
+++ b/analyzer/parser/fight_results/fight_results.c
@@ -0,0 +1,260 @@
+#include "parser/fight_results/fight_results.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "csv/id_to_artifact_size.h"
+#include "csv/id_to_item_name.h"
+#include "csv/id_to_orb_name.h"
+#include "csv/id_to_saturation_type.h"
+#include "parser/fight_results/drif.h"
+#include "parser/fight_results/entity.h"
+#include "parser/fight_results/gear.h"
+#include "parser/fight_results/gear_info.h"
+#include "parser/fight_results/orb.h"
+#include "parser/structs.h"
+#include "utils/memory.h"
+#include "utils/roman.h"
+
+// function to cleanup fight results to be used in ParsedData struct
+void cleanup_fight_results(void *data) {
+ FightResults *fight_results = data;
+ for (size_t i = 0; i < fight_results->num_friendly; i++) {
+ FriendlyResults friendly = fight_results->friendly_results[i];
+ free(friendly.name);
+ for (size_t j = 0; j < friendly.num_items; j++)
+ free(friendly.items[j].data);
+ free(friendly.items);
+ }
+ for (size_t i = 0; i < fight_results->num_enemy; i++) {
+ free(fight_results->enemy_results[i].name);
+ }
+ free(fight_results->friendly_results);
+ free(fight_results->enemy_results);
+}
+
+byte_t *find_fight_results(TCPStream *stream, int offset, int *length) {
+ byte_t *start = memmem(stream->data + offset, stream->len - offset, FIGHT_RESULTS_START, strlen(FIGHT_RESULTS_START));
+ byte_t *end = NULL;
+ if (start)
+ end = memmem(start, stream->len - (start - stream->data), FIGHT_RESULTS_END, strlen(FIGHT_RESULTS_END));
+ *length = end ? end - start + strlen(FIGHT_RESULTS_END) : -1;
+ return start;
+}
+
+void get_stars(char *buffer, int incr_above_b1) {
+ char star_types[] = {'B', 'S', 'G'};
+ sprintf(buffer, " (%c%d)", star_types[incr_above_b1 / 3], incr_above_b1 % 3 + 1);
+}
+
+void add_gear(FriendlyResults *results, size_t *items_capacity, char *gears_str) {
+ if (strlen(gears_str) == 0)
+ return;
+
+ char *gears_separator;
+ do {
+ gears_separator = strstr(gears_str, SEPARATOR_GEAR);
+ size_t len = gears_separator ? gears_separator - gears_str : strlen(gears_str);
+ Gear gear = {0};
+ fill_struct(&gear, gear_fields, ARR_LEN(gear_fields), gears_str, len, SEPARATOR_GEAR_FIELD);
+
+ // if there are still gears - modify gears_str pointer
+ if (gears_separator)
+ gears_str = gears_separator + strlen(SEPARATOR_GEAR);
+
+ // get gear info for rank, name and stars / syng level
+ GearInfo gear_info = {0};
+ fill_struct(&gear_info, gear_info_fields, ARR_LEN(gear_info_fields), gear.info, strlen(gear.info), SEPARATOR_GEAR_INFO_FIELD);
+
+ // create the buffer of information about gear
+ char buffer[SINGLE_ITEM_BUFFER_LEN];
+ char roman[5];
+ to_roman(roman, gear_info.rank);
+ sprintf(buffer, "[%s] %s", roman, gear_info.name);
+ switch (gear.type) {
+ case TYPE_SYNG_NORMAL:
+ // atoi should be fine, since syng_tier starts from 1 for syngs
+ if (atoi(gear.syng_tier)) {
+ char lvl[7];
+ sprintf(lvl, " (%d)", gear_info.lvl);
+ strcat(buffer, lvl);
+ }
+ break;
+ default: {
+ char stars[6];
+ get_stars(stars, gear_info.stars);
+ strcat(buffer, stars);
+ break;
+ }
+ }
+
+ // add gear with specific type
+ Item item = {.data = strdup(buffer)};
+ switch (gear.type) {
+ case TYPE_SYNG_NORMAL:
+ // atoi should be fine, since syng_tier starts from 1 for syngs
+ item.type = atoi(gear.syng_tier) ? SYNG : NORMAL;
+ break;
+ case TYPE_SET:
+ item.type = SET;
+ break;
+ case TYPE_RARE:
+ item.type = RARE;
+ break;
+ default:
+ item.type = EPIC;
+ break;
+ }
+ results->items = array_append(results->items, sizeof(Item), &results->num_items, items_capacity, &item);
+
+ cleanup_struct(&gear_info, gear_info_fields, ARR_LEN(gear_info_fields));
+
+ // add orb to items if it exists
+ if (strlen(gear.orb)) {
+ Orb orb = {0};
+ fill_struct(&orb, orb_fields, ARR_LEN(orb_fields), gear.orb, strlen(gear.orb), SEPARATOR_ORB_FIELD);
+
+ char buffer[SINGLE_ITEM_BUFFER_LEN];
+ sprintf(buffer, "%sorb %s", get_artifact_size_from_id(orb.size), get_orb_name_from_id(orb.id));
+
+ Item item = {.data = strdup(buffer), .type = ORB};
+ results->items = array_append(results->items, sizeof(Item), &results->num_items, items_capacity, &item);
+
+ cleanup_struct(&orb, orb_fields, ARR_LEN(orb_fields));
+ }
+
+ cleanup_struct(&gear, gear_fields, ARR_LEN(gear_fields));
+ } while (gears_separator);
+}
+
+void add_drifs(FriendlyResults *results, size_t *items_capacity, char *drifs_str) {
+ if (strlen(drifs_str) == 0)
+ return;
+
+ char *drifs_separator;
+ do {
+ drifs_separator = strstr(drifs_str, SEPARATOR_DRIF);
+ size_t len = drifs_separator ? drifs_separator - drifs_str : strlen(drifs_str);
+ Drif drif = {0};
+ fill_struct(&drif, drif_fields, ARR_LEN(drif_fields), drifs_str, len, SEPARATOR_DRIF_FIELD);
+
+ // if there are still drifs - modify drifs_str pointer
+ if (drifs_separator)
+ drifs_str = drifs_separator + strlen(SEPARATOR_DRIF);
+
+ Item item = {.data = strdup(drif.name), .type = DRIF};
+ results->items = array_append(results->items, sizeof(Item), &results->num_items, items_capacity, &item);
+
+ cleanup_struct(&drif, drif_fields, ARR_LEN(drif_fields));
+ } while (drifs_separator);
+}
+
+void add_items(FriendlyResults *results, size_t *items_capacity, char *items_str) {
+ if (strlen(items_str) == 0)
+ return;
+
+ char *items_separator;
+ do {
+ char buffer[SINGLE_ITEM_BUFFER_LEN];
+ items_separator = strstr(items_str, SEPARATOR_ITEMS);
+
+ int item_id, item_count;
+ sscanf(items_str, "%d,%d", &item_id, &item_count);
+ sprintf(buffer, "%dx %s", item_count, get_item_name_from_id(item_id));
+
+ // if there are still items - modify items_str pointer
+ if (items_separator)
+ items_str = items_separator + strlen(SEPARATOR_ITEMS);
+
+ Item item = {.data = strdup(buffer), .type = NORMAL};
+ results->items = array_append(results->items, sizeof(Item), &results->num_items, items_capacity, &item);
+ } while (items_separator);
+}
+
+// parse EntityResults to FriendlyResults
+FriendlyResults parse_to_friendly(EntityResults entity_results) {
+ FriendlyResults results = {
+ .name = strdup(entity_results.name),
+ .exp = entity_results.exp,
+ .gold = entity_results.gold,
+ .level = entity_results.level,
+ .psycho = entity_results.psycho,
+ .splinters = entity_results.splinters,
+ .num_items = 0,
+ .saturation = 0
+ };
+
+ // create items array and add to it gear, drifs and items
+ results.items = safe_malloc(INIT_ITEMS_CAPACITY * sizeof(Item));
+ size_t items_cap = INIT_ITEMS_CAPACITY;
+ add_gear(&results, &items_cap, entity_results.gear);
+ add_drifs(&results, &items_cap, entity_results.drifs);
+ add_items(&results, &items_cap, entity_results.items);
+
+ if (strlen(entity_results.saturation) > 0) {
+ int saturation_id;
+ sscanf(entity_results.saturation, "%d,%d", &saturation_id, &results.saturation);
+ results.saturation_type = get_saturation_type_from_id(saturation_id);
+ }
+
+ return results;
+}
+
+// parse EntityResults to EnemyResults
+EnemyResults parse_to_enemy(EntityResults entity_results) {
+ EnemyResults results = {.name = strdup(entity_results.name), .level = entity_results.level};
+ return results;
+}
+
+ParsedData parse_fight_results(byte_t *data, size_t length) {
+ ParsedData parsed_data = {.data_type = TYPE_FIGHT_RESULTS, .data_cleanup = cleanup_fight_results};
+
+ // ignore start and end indicators
+ data += strlen(FIGHT_RESULTS_START);
+ length -= strlen(FIGHT_RESULTS_START) + strlen(FIGHT_RESULTS_END);
+
+ FightResults *results = safe_malloc(sizeof(FightResults));
+
+ results->friendly_results = safe_malloc(INIT_ENTITIES_PER_SIDE * sizeof(FriendlyResults));
+ results->num_friendly = 0;
+ size_t friendly_cap = INIT_ENTITIES_PER_SIDE;
+
+ results->enemy_results = safe_malloc(INIT_ENTITIES_PER_SIDE * sizeof(EnemyResults));
+ results->num_enemy = 0;
+ size_t enemy_cap = INIT_ENTITIES_PER_SIDE;
+
+ byte_t *entity_separator;
+ do {
+ // get generic entity results
+ entity_separator = memmem(data, length, SEPARATOR_ENTITY, strlen(SEPARATOR_ENTITY));
+ size_t len = entity_separator ? entity_separator - data : length;
+ EntityResults entity_results = {0};
+ fill_struct(&entity_results, entity_fields, ARR_LEN(entity_fields), (char *)data, len, SEPARATOR_ENTITY_FIELD);
+
+ // depending on team - parse it into friendly or enemy results
+ switch (entity_results.team) {
+ case TEAM_FRIEND: {
+ FriendlyResults res = parse_to_friendly(entity_results);
+ results->friendly_results = array_append(results->friendly_results, sizeof(FriendlyResults), &results->num_friendly, &friendly_cap, &res);
+ break;
+ }
+ case TEAM_ENEMY: {
+ EnemyResults res = parse_to_enemy(entity_results);
+ results->enemy_results = array_append(results->enemy_results, sizeof(EnemyResults), &results->num_enemy, &enemy_cap, &res);
+ break;
+ }
+ }
+
+ cleanup_struct(&entity_results, entity_fields, ARR_LEN(entity_fields));
+
+ // if there are still entities - modify data pointer and length
+ if (entity_separator) {
+ length -= entity_separator - data + strlen(SEPARATOR_ENTITY);
+ data = entity_separator + strlen(SEPARATOR_ENTITY);
+ }
+ } while (entity_separator);
+
+ parsed_data.data = results;
+ return parsed_data;
+}
diff --git a/analyzer/parser/fight_results/fight_results.h b/analyzer/parser/fight_results/fight_results.h
new file mode 100644
index 0000000..ec90394
--- /dev/null
+++ b/analyzer/parser/fight_results/fight_results.h
@@ -0,0 +1,60 @@
+// parser/fight_results.h
+// utilities for parsing the fight results data
+
+#include "parser/parser_defs.h"
+#include "utils/tcp.h"
+
+#ifndef FIGHT_RESULTS_H
+#define FIGHT_RESULTS_H
+
+// Payload structure:
+// FIGHT_RESULTS_START EntityResults (SEPARATOR_ENTITY EntityResults)* FIGHT_RESULTS_END
+#define FIGHT_RESULTS_START "3;19;"
+#define FIGHT_RESULTS_END "[--];"
+
+// items parsing info
+#define INIT_ITEMS_CAPACITY 8
+#define SINGLE_ITEM_BUFFER_LEN 128
+#define SEPARATOR_ITEMS "[-]"
+
+// Item struct - for keeping item string and type
+typedef enum { NORMAL, DRIF, ORB, SYNG, SET, RARE, EPIC } ItemType;
+typedef struct {
+ char *data;
+ ItemType type;
+} Item;
+
+// fight results data for friendly
+typedef struct {
+ char *name;
+ int exp;
+ int gold;
+ Item *items;
+ size_t num_items;
+ int level;
+ int psycho;
+ int splinters;
+ const char *saturation_type;
+ int saturation;
+} FriendlyResults;
+
+// fight results data for enemy
+typedef struct {
+ char *name;
+ int level;
+} EnemyResults;
+
+#define INIT_ENTITIES_PER_SIDE 6
+
+// entire fight resutls
+typedef struct {
+ size_t num_friendly;
+ FriendlyResults *friendly_results;
+ size_t num_enemy;
+ EnemyResults *enemy_results;
+} FightResults;
+
+extern find_function_t find_fight_results;
+extern parse_function_t parse_fight_results;
+
+#endif
diff --git a/analyzer/parser/fight_results/gear.h b/analyzer/parser/fight_results/gear.h
new file mode 100644
index 0000000..66f52f3
--- /dev/null
+++ b/analyzer/parser/fight_results/gear.h
@@ -0,0 +1,45 @@
+// parser/fight_results/gear.h
+// Gear generic struct data
+
+#include "parser/structs.h"
+
+// Gear structure:
+// field (SEPARATOR_GEAR_FIELD field)*
+// fields are defined in order in the Gear struct
+#define SEPARATOR_GEAR "???" // TODO: change it to correct one which idk at this moment
+#define SEPARATOR_GEAR_FIELD "$"
+
+// values of field "type" in Gear
+#define TYPE_SYNG_NORMAL 0
+#define TYPE_SET 1
+#define TYPE_RARE 2
+
+typedef struct {
+ char *stats; // strength, dexterity, power, knowledge, hp, mana, stamina
+ char *resistances; // slashing, crushing, piercing, fire, cold, energy, mental
+ char *info; // GearInfo struct data
+ char *reqs; // requirements (str, dex, pow, know), weight, something else
+ char *field5;
+ char *psycho; // psycho effects (id,%) for syngs
+ char *field7;
+ char *field8;
+ char *field9;
+ char *syng_tier; // lvl 20 - 1, lvl 30 - 2, etc..., null for non-syngs
+ int field11;
+ int field12;
+ int field13;
+ char *field14;
+ int type; // 0 - normal item or syng, 1 - set, 2 - rare
+ int field16; // 10 for rare, -1 for others ???
+ char *orb; // orb info
+ char *field18;
+ char *field19;
+} Gear;
+
+static const StructField gear_fields[] = {
+ {STRING, offsetof(Gear, stats)}, {STRING, offsetof(Gear, resistances)}, {STRING, offsetof(Gear, info)}, {STRING, offsetof(Gear, reqs)},
+ {STRING, offsetof(Gear, field5)}, {STRING, offsetof(Gear, psycho)}, {STRING, offsetof(Gear, field7)}, {STRING, offsetof(Gear, field8)},
+ {STRING, offsetof(Gear, field9)}, {STRING, offsetof(Gear, syng_tier)}, {INT, offsetof(Gear, field11)}, {INT, offsetof(Gear, field12)},
+ {INT, offsetof(Gear, field13)}, {STRING, offsetof(Gear, field14)}, {INT, offsetof(Gear, type)}, {INT, offsetof(Gear, field16)},
+ {STRING, offsetof(Gear, orb)}, {STRING, offsetof(Gear, field18)}, {STRING, offsetof(Gear, field19)},
+};
diff --git a/analyzer/parser/fight_results/gear_info.h b/analyzer/parser/fight_results/gear_info.h
new file mode 100644
index 0000000..f0cf83a
--- /dev/null
+++ b/analyzer/parser/fight_results/gear_info.h
@@ -0,0 +1,44 @@
+// parser/fight_results/gear_info.h
+// GearInfo generic struct data
+
+#include "parser/structs.h"
+
+// GearInfo structure:
+// field (SEPARATOR_GEAR_INFO_FIELD field)*
+// fields are defined in order in the GearInfo struct
+#define SEPARATOR_GEAR_INFO_FIELD ","
+
+typedef struct {
+ char *name; // item name
+ int field2;
+ int lvl; // level requirement
+ int price; // gold price value
+ int field5;
+ int field6;
+ int field7;
+ int rank; // I-XII
+ int field9;
+ int field10;
+ int field11;
+ int durability; // in %
+ char *field13;
+ int field14;
+ int field15;
+ float charges; // charge count
+ char *field17;
+ int field18;
+ int stars; // incrustation level: 0 - b1, 1 - b2, ... (-100 for syngs)
+ int field20;
+ int field21;
+ int field22;
+ int field23;
+} GearInfo;
+
+static const StructField gear_info_fields[] = {
+ {STRING, offsetof(GearInfo, name)}, {INT, offsetof(GearInfo, field2)}, {INT, offsetof(GearInfo, lvl)}, {INT, offsetof(GearInfo, price)},
+ {INT, offsetof(GearInfo, field5)}, {INT, offsetof(GearInfo, field6)}, {INT, offsetof(GearInfo, field7)}, {INT, offsetof(GearInfo, rank)},
+ {INT, offsetof(GearInfo, field9)}, {INT, offsetof(GearInfo, field10)}, {INT, offsetof(GearInfo, field11)}, {INT, offsetof(GearInfo, durability)},
+ {STRING, offsetof(GearInfo, field13)}, {INT, offsetof(GearInfo, field14)}, {INT, offsetof(GearInfo, field15)}, {FLOAT, offsetof(GearInfo, charges)},
+ {STRING, offsetof(GearInfo, field17)}, {INT, offsetof(GearInfo, field18)}, {INT, offsetof(GearInfo, stars)}, {INT, offsetof(GearInfo, field20)},
+ {INT, offsetof(GearInfo, field21)}, {INT, offsetof(GearInfo, field22)}, {INT, offsetof(GearInfo, field23)},
+};
diff --git a/analyzer/parser/fight_results/orb.h b/analyzer/parser/fight_results/orb.h
new file mode 100644
index 0000000..dcf146f
--- /dev/null
+++ b/analyzer/parser/fight_results/orb.h
@@ -0,0 +1,33 @@
+// parser/fight_results/orb.h
+// Orb generic struct data
+
+#include "parser/structs.h"
+
+// Orb structure:
+// field (SEPARATOR_ORB_FIELD field)*
+// fields are defined in order in the Orb struct
+#define SEPARATOR_ORB_FIELD ","
+
+typedef struct {
+ char *field1; // just says "orb"
+ int field2;
+ int field3;
+ char *field4;
+ int field5;
+ int id; // id of orb type
+ int size; // 1 - sub, 2 - bi, 3 - magni, 4 - arcy
+ float value; // value in % you gain from the orb
+ char *field9;
+} Orb;
+
+static const StructField orb_fields[] = {
+ {STRING, offsetof(Orb, field1)},
+ {INT, offsetof(Orb, field2)},
+ {INT, offsetof(Orb, field3)},
+ {STRING, offsetof(Orb, field4)},
+ {INT, offsetof(Orb, field5)},
+ {INT, offsetof(Orb, id)},
+ {INT, offsetof(Orb, size)},
+ {FLOAT, offsetof(Orb, value)},
+ {STRING, offsetof(Orb, field9)},
+};
diff --git a/analyzer/parser/find_parse_functions.h b/analyzer/parser/find_parse_functions.h
new file mode 100644
index 0000000..e2c2acb
--- /dev/null
+++ b/analyzer/parser/find_parse_functions.h
@@ -0,0 +1,6 @@
+// parser/find_parse_functions.h
+// array of find and parse functions for every result type
+
+#include "parser/fight_results/fight_results.h"
+
+FindParseFunctions all_find_parse_functions[] = {{find_fight_results, parse_fight_results}};
diff --git a/analyzer/parser/parser.c b/analyzer/parser/parser.c
new file mode 100644
index 0000000..d00cd97
--- /dev/null
+++ b/analyzer/parser/parser.c
@@ -0,0 +1,64 @@
+#include "parser/parser.h"
+
+#include <stdlib.h>
+
+#include "parser/find_parse_functions.h"
+#include "utils/memory.h"
+
+// find earliest data to parse in stream
+byte_t *find_earliest_data(TCPStream *stream, int stream_offset, int *earliest_length, parse_function_t **earliest_parse_fun) {
+ byte_t *earliest_data = NULL;
+ for (size_t i = 0; i < ARR_LEN(all_find_parse_functions); i++) {
+ int length;
+ byte_t *data = all_find_parse_functions[i].find_fn(stream, stream_offset, &length);
+ // if it's the first found or earlier than earliest - save it
+ if (data && (!earliest_data || data < earliest_data)) {
+ earliest_data = data;
+ *earliest_length = length;
+ *earliest_parse_fun = all_find_parse_functions[i].parse_fn;
+ }
+ }
+ return earliest_data;
+}
+
+ParsedData *parse_stream(TCPStream *stream, size_t *parsed_len, size_t *n_parsed_data) {
+ size_t parsed_data_capacity = INIT_PARSED_DATA_CAPACITY;
+ ParsedData *parsed_data = safe_malloc(parsed_data_capacity * sizeof(ParsedData));
+ *n_parsed_data = 0;
+
+ int stream_offset = 0;
+ byte_t *earliest_data;
+ do {
+ // find earliest data to parse
+ int earliest_length;
+ parse_function_t *earliest_parse_fun;
+ earliest_data = find_earliest_data(stream, stream_offset, &earliest_length, &earliest_parse_fun);
+
+ // if start of data to parse was found, but not the end, then finish here
+ // set parsed_len, so that the beginning of that data is at the beginning of the stream
+ if (earliest_data && earliest_length == -1) {
+ *parsed_len = earliest_data - stream->data;
+ return parsed_data;
+ }
+
+ // if data to parse was found, parse it and update the offset
+ if (earliest_data) {
+ ParsedData new_parsed = earliest_parse_fun(earliest_data, earliest_length);
+ parsed_data = array_append(parsed_data, sizeof(ParsedData), n_parsed_data, &parsed_data_capacity, &new_parsed);
+ stream_offset = earliest_data - stream->data + earliest_length;
+ }
+ } while (earliest_data);
+
+ // if all was parsed - set parsed_len to stream_len and return the data
+ *parsed_len = stream->len;
+ return parsed_data;
+}
+
+void free_parsed_data(ParsedData *parsed_data, size_t n_parsed_data) {
+ for (size_t i = 0; i < n_parsed_data; i++) {
+ if (parsed_data[i].data_cleanup)
+ parsed_data[i].data_cleanup(parsed_data[i].data);
+ free(parsed_data[i].data);
+ }
+ free(parsed_data);
+}
diff --git a/analyzer/parser/parser.h b/analyzer/parser/parser.h
new file mode 100644
index 0000000..e7c46e8
--- /dev/null
+++ b/analyzer/parser/parser.h
@@ -0,0 +1,23 @@
+// parser/parser.h
+// main parser file
+
+#include "parser/fight_results/fight_results.h"
+#include "parser/parser_defs.h"
+#include "utils/tcp.h"
+
+#define INIT_PARSED_DATA_CAPACITY 4
+
+// parse a stream of TCP data
+// params:
+// - stream - the stream to parse
+// - parsed_len - pointer to inform how much of the stream was parsed (not all if e.g. start of data is there, but the end isn't yet)
+// - n_parsed_data - pointer to the count of parsed data that was retrieved from the stream
+// returns:
+// pointer to the array of parsed data
+extern ParsedData *parse_stream(TCPStream *stream, size_t *parsed_len, size_t *n_parsed_data);
+
+// free the entire parsed data array from parse_stream function
+// params:
+// - parsed_data - parsed data to free
+// - n_parsed_data - count of parsed data
+extern void free_parsed_data(ParsedData *parsed_data, size_t n_parsed_data);
diff --git a/analyzer/parser/parser_defs.h b/analyzer/parser/parser_defs.h
new file mode 100644
index 0000000..75b0366
--- /dev/null
+++ b/analyzer/parser/parser_defs.h
@@ -0,0 +1,42 @@
+// parser/parser_defs.h
+// useful definitions for parser files
+
+#include "utils/tcp.h"
+
+#ifndef PARSER_DEFS_H
+#define PARSER_DEFS_H
+
+typedef enum { TYPE_FIGHT_RESULTS } ResultsEnum;
+
+// "generic" type for parsed data
+typedef struct {
+ ResultsEnum data_type; // type of data represented by an enum
+ void *data; // pointer to the parsed data
+ void (*data_cleanup)(void *data); // function to cleanup internal allocations inside data
+ // if no internal allocations - this should be NULL
+} ParsedData;
+
+// find the first results data of specific type in stream
+// params:
+// - stream - stream to find the data in
+// - offset - offset to start finding data from
+// - length - length of the data or -1 if there's no end yet
+// returns:
+// pointer to the beginning of data or NULL if there's none
+typedef byte_t *find_function_t(TCPStream *stream, int offset, int *length);
+
+// parse given data of specifc type
+// params:
+// - data - data to parse
+// - length - length of data
+// returns:
+// ParsedData struct including the parsed data
+typedef ParsedData parse_function_t(byte_t *data, size_t length);
+
+// struct for holding the find and parse function for every result type
+typedef struct {
+ find_function_t *find_fn;
+ parse_function_t *parse_fn;
+} FindParseFunctions;
+
+#endif
diff --git a/analyzer/parser/structs.c b/analyzer/parser/structs.c
new file mode 100644
index 0000000..dedf0d1
--- /dev/null
+++ b/analyzer/parser/structs.c
@@ -0,0 +1,51 @@
+#include "parser/structs.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "utils/memory.h"
+
+void fill_struct(void *obj, const StructField *fields, size_t n_fields, const char *data, size_t data_len, const char *sep) {
+ for (size_t i = 0; i < n_fields; i++) {
+ char *next_sep = memmem(data, data_len, sep, strlen(sep));
+ size_t len = next_sep ? next_sep - data : data_len;
+
+ char *str = safe_malloc(len + 1);
+ memcpy(str, data, len);
+ str[len] = '\0';
+
+ // parse depending on variable type
+ void *p_field = (char *)obj + fields[i].offset;
+ switch (fields[i].type) {
+ case INT:
+ sscanf(str, "%d", (int *)p_field);
+ free(str);
+ break;
+ case FLOAT:
+ sscanf(str, "%f", (float *)p_field);
+ free(str);
+ break;
+ case DOUBLE:
+ sscanf(str, "%lf", (double *)p_field);
+ free(str);
+ break;
+ case STRING:
+ *(char **)p_field = str;
+ break;
+ }
+
+ // update data pointer and length
+ if (next_sep) {
+ data_len -= next_sep - data + strlen(sep);
+ data = next_sep + strlen(sep);
+ }
+ }
+}
+
+void cleanup_struct(void *obj, const StructField *fields, size_t n_fields) {
+ for (size_t i = 0; i < n_fields; i++) {
+ if (fields[i].type == STRING)
+ free(*(char **)((char *)obj + fields[i].offset));
+ }
+}
diff --git a/analyzer/parser/structs.h b/analyzer/parser/structs.h
new file mode 100644
index 0000000..023c19b
--- /dev/null
+++ b/analyzer/parser/structs.h
@@ -0,0 +1,33 @@
+// parser/structs.h
+// generic struct functionality for parsing
+
+#include <stddef.h>
+
+#ifndef PARSER_STRUCTS_H
+#define PARSER_STRUCTS_H
+
+typedef enum { INT, FLOAT, DOUBLE, STRING } TypeEnum;
+
+typedef struct {
+ TypeEnum type;
+ size_t offset;
+} StructField;
+
+// fill generic struct with given data
+// params:
+// - obj - pointer to the struct to fill
+// - fields - field types and offsets of the struct
+// - n_fields - count of fields in the struct
+// - data - data to fill the struct with
+// - data_len - length of the data
+// - sep - separator seperating the fields in data
+extern void fill_struct(void *obj, const StructField *fields, size_t n_fields, const char *data, size_t data_len, const char *sep);
+
+// cleanup allocations inside generic struct
+// params:
+// - obj - pointer to the struct to cleanup
+// - fields - field types and offsets of the struct
+// - n_fields - count of fields in the struct
+extern void cleanup_struct(void *obj, const StructField *fields, size_t n_fields);
+
+#endif
diff --git a/analyzer/render.c b/analyzer/render.c
new file mode 100644
index 0000000..5c4b2d0
--- /dev/null
+++ b/analyzer/render.c
@@ -0,0 +1,162 @@
+#include "render.h"
+
+#include <stdio.h>
+#include <string.h>
+
+// strlen counting utf-8 characters as one
+size_t utf8_strlen(char *s) {
+ size_t len = 0;
+ while (*s) {
+ // if the byte is not a continuation byte (10xxxxxx), it's a new character
+ if ((*s & 0xC0) != 0x80)
+ len++;
+ s++;
+ }
+ return len;
+}
+
+void print_row_separator() {
+ for (size_t i = 0; i < TABLE_LINE_WIDTH; i++)
+ printf("%c", TABLE_ROW_SEPARATOR_CHAR);
+ printf("\n");
+}
+
+// print string with given color
+void print_color(char *str, int color) {
+ printf("\033[%dm%s\033[0m", color, str);
+}
+
+void print_center(char *text, int color, size_t width, char padding_char) {
+ size_t post_char_count = (width - strlen(text)) / 2;
+ size_t pre_char_count = width - post_char_count - strlen(text);
+
+ for (size_t i = 0; i < pre_char_count; i++)
+ printf("%c", padding_char);
+
+ print_color(text, color);
+
+ for (size_t i = 0; i < post_char_count; i++)
+ printf("%c", padding_char);
+}
+
+void print_enemy_list(EnemyResults *results, size_t n_enemies) {
+ printf(TABLE_LINE_START);
+ size_t line_length = TABLE_LINE_START_WIDTH;
+
+ for (size_t i = 0; i < n_enemies; i++) {
+ EnemyResults enemy = results[i];
+ char enemy_buffer[TABLE_LINE_WIDTH + 1];
+ sprintf(enemy_buffer, "%s (%d)%s", enemy.name, enemy.level, i < n_enemies - 1 ? ", " : "");
+
+ // break line if length would be too big
+ if (line_length + utf8_strlen(enemy_buffer) > TABLE_LINE_WIDTH - TABLE_LINE_END_WIDTH) {
+ printf("%*s\n", (int)(TABLE_LINE_WIDTH - line_length), TABLE_LINE_END);
+ printf(TABLE_LINE_START);
+ line_length = TABLE_LINE_START_WIDTH;
+ }
+
+ printf("%s", enemy_buffer);
+ line_length += utf8_strlen(enemy_buffer);
+ }
+
+ printf("%*s\n", (int)(TABLE_LINE_WIDTH - line_length), TABLE_LINE_END);
+}
+
+void print_column_names() {
+ printf(TABLE_LINE_START);
+ for (size_t i = 0; i < ARR_LEN(columns); i++) {
+ print_center(columns[i].name, columns[i].color, columns[i].width, ' ');
+ printf(i < ARR_LEN(columns) - 1 ? TABLE_LINE_SEPARATOR : TABLE_LINE_END);
+ }
+ printf("\n");
+}
+
+int item_type_to_color(ItemType type) {
+ switch (type) {
+ case NORMAL:
+ return COLOR_WHITE;
+ case DRIF:
+ return COLOR_YELLOW;
+ case ORB:
+ return COLOR_GOLD;
+ case SYNG:
+ return COLOR_RED;
+ case SET:
+ return COLOR_GREEN;
+ case RARE:
+ return COLOR_AQUA;
+ case EPIC:
+ return COLOR_PURPLE;
+ }
+ return COLOR_WHITE;
+}
+
+void print_player_rewards(FriendlyResults results) {
+ char buffers[ARR_LEN(columns)][TABLE_LINE_WIDTH];
+
+ sprintf(buffers[0], "%s (%d)", results.name, results.level);
+ sprintf(buffers[1], "%d", results.exp);
+ sprintf(buffers[2], "%d", results.gold);
+
+ if (results.psycho > 0)
+ sprintf(buffers[3], "%d", results.psycho);
+ else
+ sprintf(buffers[3], "-");
+
+ if (results.saturation > 0)
+ sprintf(buffers[4], "%d (%s)", results.saturation, results.saturation_type);
+ else
+ sprintf(buffers[4], "-");
+
+ if (results.splinters > 0)
+ sprintf(buffers[5], "%d", results.splinters);
+ else
+ sprintf(buffers[5], "-");
+
+ printf(TABLE_LINE_START);
+ for (size_t i = 0; i < ARR_LEN(columns); i++) {
+ char to_color[columns[i].width + 1];
+ sprintf(to_color, "%*s", (int)columns[i].width, buffers[i]);
+ print_color(to_color, columns[i].color);
+ printf(i < ARR_LEN(columns) - 1 ? TABLE_LINE_SEPARATOR : TABLE_LINE_END);
+ }
+ printf("\n");
+
+ if (results.num_items > 0) {
+ printf(TABLE_LINE_START);
+ print_center("ITEMS", COLOR_WHITE, TABLE_ITEMS_WIDTH, ' ');
+ printf(TABLE_LINE_END "\n" TABLE_LINE_START);
+
+ size_t line_len = 0;
+ for (size_t i = 0; i < results.num_items; i++) {
+ char *sep = i < results.num_items - 1 ? ", " : "";
+ size_t item_len = utf8_strlen(results.items[i].data) + strlen(sep);
+ if (line_len + item_len > TABLE_ITEMS_WIDTH) {
+ printf("%*s" TABLE_LINE_END "\n" TABLE_LINE_START, (int)(TABLE_ITEMS_WIDTH - line_len), "");
+ line_len = 0;
+ }
+ print_color(results.items[i].data, item_type_to_color(results.items[i].type));
+ printf("%s", sep);
+ line_len += item_len;
+ }
+ printf("%*s" TABLE_LINE_END "\n", (int)(TABLE_ITEMS_WIDTH - line_len), "");
+ }
+}
+
+void render_fight_results(FightResults results) {
+ print_center("ENEMIES", COLOR_RED, TABLE_LINE_WIDTH, TABLE_ROW_SEPARATOR_CHAR);
+ printf("\n");
+
+ print_enemy_list(results.enemy_results, results.num_enemy);
+
+ print_center("PLAYER REWARDS", COLOR_GREEN, TABLE_LINE_WIDTH, TABLE_ROW_SEPARATOR_CHAR);
+ printf("\n");
+
+ print_column_names();
+ print_row_separator();
+
+ for (size_t i = 0; i < results.num_friendly; i++) {
+ print_player_rewards(results.friendly_results[i]);
+ print_row_separator();
+ }
+}
diff --git a/analyzer/render.h b/analyzer/render.h
new file mode 100644
index 0000000..911ca76
--- /dev/null
+++ b/analyzer/render.h
@@ -0,0 +1,55 @@
+// render.h
+// functions for "rendering" data onto the terminal
+
+#include "parser/fight_results/fight_results.h"
+#include "utils/memory.h"
+
+// ANSI escape sequence colors
+#define COLOR_GOLD 33
+#define COLOR_GRAY 90
+#define COLOR_RED 91
+#define COLOR_GREEN 92
+#define COLOR_YELLOW 93
+#define COLOR_BLUE 94
+#define COLOR_PURPLE 95
+#define COLOR_AQUA 96
+#define COLOR_WHITE 97
+
+#define TABLE_ROW_SEPARATOR_CHAR '-'
+
+#define TABLE_LINE_START "| "
+#define TABLE_LINE_END " |"
+#define TABLE_LINE_SEPARATOR " | "
+#define TABLE_LINE_START_WIDTH (sizeof(TABLE_LINE_START) - 1)
+#define TABLE_LINE_END_WIDTH (sizeof(TABLE_LINE_END) - 1)
+#define TABLE_LINE_SEPARATOR_WIDTH (sizeof(TABLE_LINE_SEPARATOR) - 1)
+
+// column info
+typedef struct {
+ char *name;
+ size_t width;
+ int color;
+} ColumnInfo;
+static const ColumnInfo columns[] = {
+ {"Nick (LVL)", 17, COLOR_WHITE},
+ {"EXP", 7, COLOR_WHITE},
+ {"Gold", 7, COLOR_YELLOW},
+ {"Psycho", 6, COLOR_AQUA},
+ {"Saturation", 13, COLOR_GREEN},
+ {"Splinters", 9, COLOR_BLUE},
+};
+static const inline size_t sum_col_widths() {
+ size_t sum = 0;
+ for (size_t i = 0; i < ARR_LEN(columns); i++)
+ sum += columns[i].width;
+ return sum;
+}
+
+#define TABLE_LINE_WIDTH (TABLE_LINE_START_WIDTH + sum_col_widths() + (ARR_LEN(columns) - 1) * TABLE_LINE_SEPARATOR_WIDTH + TABLE_LINE_END_WIDTH)
+
+#define TABLE_ITEMS_WIDTH (TABLE_LINE_WIDTH - TABLE_LINE_START_WIDTH - TABLE_LINE_END_WIDTH)
+
+// render fight results onto the terminal
+// params:
+// - results - fight results to render
+extern void render_fight_results(FightResults results);
diff --git a/analyzer/structures/hashmap.c b/analyzer/structures/hashmap.c
new file mode 100644
index 0000000..5eb27ae
--- /dev/null
+++ b/analyzer/structures/hashmap.c
@@ -0,0 +1,116 @@
+#include "structures/hashmap.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "utils/memory.h"
+
+HashMap *hashmap_new(
+ size_t key_size,
+ size_t value_size,
+ size_t (*hash)(void *key),
+ int (*compare)(void *key1, void *key2),
+ void (*key_cleanup)(void *key),
+ void (*value_cleanup)(void *key)
+) {
+ HashMap *map = safe_malloc(sizeof(HashMap));
+ // initialize NULLs
+ memset(map->table, 0, HASHMAP_TABLE_SIZE * sizeof(HashMapNode *));
+ map->key_size = key_size;
+ map->value_size = value_size;
+ map->hash = hash;
+ map->compare = compare;
+ map->key_cleanup = key_cleanup;
+ map->value_cleanup = value_cleanup;
+ return map;
+}
+
+void hashmap_put(HashMap *map, void *key, void *value) {
+ size_t key_hash = map->hash(key);
+ HashMapNode *node = map->table[key_hash];
+
+ // check if it exists
+ while (node) {
+ // if it exists - update node
+ if (map->compare(key, node->key)) {
+ // ensure no hanging memory allocation
+ if (map->value_cleanup) {
+ map->value_cleanup(node->value);
+ }
+ memcpy(node->value, value, map->value_size);
+ return;
+ }
+ node = node->next;
+ }
+
+ // if it doesn't exist - create new node
+ HashMapNode *new_node = safe_malloc(sizeof(HashMapNode));
+ new_node->key = safe_malloc(map->key_size);
+ new_node->value = safe_malloc(map->value_size);
+ memcpy(new_node->key, key, map->key_size);
+ memcpy(new_node->value, value, map->value_size);
+
+ // put in front of the linked list
+ new_node->next = map->table[key_hash];
+ map->table[key_hash] = new_node;
+}
+
+void *hashmap_get(HashMap *map, void *key) {
+ size_t key_hash = map->hash(key);
+ HashMapNode *node = map->table[key_hash];
+
+ // check if it exists
+ while (node) {
+ // if it exists - return the value pointer
+ if (map->compare(key, node->key)) {
+ return node->value;
+ }
+ node = node->next;
+ }
+
+ // if it doesn't exist - return NULL
+ return NULL;
+}
+
+void free_node_without_next(HashMap *map, HashMapNode *node) {
+ if (map->key_cleanup) {
+ map->key_cleanup(node->key);
+ }
+ if (map->value_cleanup) {
+ map->value_cleanup(node->value);
+ }
+ free(node->key);
+ free(node->value);
+ free(node);
+}
+
+void hashmap_delete(HashMap *map, void *key) {
+ size_t key_hash = map->hash(key);
+ HashMapNode **p_node = &map->table[key_hash];
+
+ // check if it exists
+ while (*p_node) {
+ // if it exists - delete it
+ if (map->compare(key, (*p_node)->key)) {
+ HashMapNode *next = (*p_node)->next;
+ free_node_without_next(map, *p_node);
+ *p_node = next;
+ return;
+ }
+ p_node = &(*p_node)->next;
+ }
+}
+
+void hashmap_free(HashMap *map) {
+ if (!map)
+ return;
+ for (size_t i = 0; i < HASHMAP_TABLE_SIZE; i++) {
+ HashMapNode *curr = map->table[i];
+ while (curr) {
+ HashMapNode *next = curr->next;
+ free_node_without_next(map, curr);
+ curr = next;
+ }
+ }
+ free(map);
+}
diff --git a/analyzer/structures/hashmap.h b/analyzer/structures/hashmap.h
new file mode 100644
index 0000000..528f24a
--- /dev/null
+++ b/analyzer/structures/hashmap.h
@@ -0,0 +1,81 @@
+// structures/hashmap.h
+// a simple implementation of hashmap using linked list
+
+#include <stddef.h>
+
+#define HASHMAP_TABLE_SIZE 128
+
+typedef struct HashMapNode {
+ void *key;
+ void *value;
+ struct HashMapNode *next;
+} HashMapNode;
+
+typedef struct {
+ HashMapNode *table[HASHMAP_TABLE_SIZE];
+ size_t key_size;
+ size_t value_size;
+ size_t (*hash)(void *key);
+ int (*compare)(void *key1, void *key2);
+ void (*key_cleanup)(void *key);
+ void (*value_cleanup)(void *value);
+} HashMap;
+
+// create a new hash map
+// params:
+// - key_size - size of the key type
+// - value_size - size of the value type
+// - hash - function for hashing keys
+// params:
+// - key - pointer to the key to hash
+// returns:
+// hash of the key (size_t in [0; HASHMAP_TABLE_SIZE))
+// - compare - function for comparing keys
+// params:
+// - key1 - pointer to the first of the keys to compare
+// - key2 - pointer to the second of the keys to compare
+// returns:
+// truthy if the keys are identical, falsy if they are different
+// - key_cleanup - function for cleaning up any internal allocations inside node->key (not the key itself)
+// if it has no heap allocations, this should be NULL
+// params:
+// - key - pointer to the key to cleanup
+// - value_cleanup - function for cleaning up any internal allocations inside node->value (not the value itself)
+// if it has no heap allocations, this should be NULL
+// params:
+// - value - pointer to the value to cleanup
+// returns:
+// pointer to the new hash map
+extern HashMap *hashmap_new(
+ size_t key_size,
+ size_t value_size,
+ size_t (*hash)(void *key),
+ int (*compare)(void *key1, void *key2),
+ void (*key_cleanup)(void *key),
+ void (*value_cleanup)(void *value)
+);
+
+// create/update an entry in a hash map
+// params:
+// - map - hash map to insert to
+// - key - pointer to the key
+// - value - pointer to the value
+extern void hashmap_put(HashMap *map, void *key, void *value);
+
+// get the value of given key from a hash map
+// params:
+// - map - hash map to get from
+// - key - pointer to the key of the value to get
+// returns:
+// pointer to the value of specified key or NULL if it doesn't exist
+extern void *hashmap_get(HashMap *map, void *key);
+
+// delete an entry from a hash map
+// params:
+// - map - hash map to delete from
+// - key - pointer to the key of the entry to delete
+extern void hashmap_delete(HashMap *map, void *key);
+
+// free the hash map
+// - map - hash map to free
+extern void hashmap_free(HashMap *map);
diff --git a/analyzer/structures/heap.c b/analyzer/structures/heap.c
new file mode 100644
index 0000000..1267668
--- /dev/null
+++ b/analyzer/structures/heap.c
@@ -0,0 +1,113 @@
+#include "structures/heap.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "utils/memory.h"
+
+Heap *heap_new(size_t element_size, int (*compare)(void *element1, void *element2), void (*element_cleanup)(void *element)) {
+ Heap *heap = safe_malloc(sizeof(Heap));
+ heap->data = safe_malloc(INIT_HEAP_CAPACITY * sizeof(void *));
+ heap->n_elements = 0;
+ heap->element_size = element_size;
+ heap->capacity = INIT_HEAP_CAPACITY;
+ heap->compare = compare;
+ heap->element_cleanup = element_cleanup;
+ return heap;
+}
+
+// return index of parent in heap
+size_t parent(size_t i) {
+ return (i - 1) / 2;
+}
+
+// return index of left child in heap
+size_t left(size_t i) {
+ return 2 * i + 1;
+}
+
+// return index of right child in heap
+size_t right(size_t i) {
+ return 2 * i + 2;
+}
+
+// swap two elements
+void swap(void **p_el1, void **p_el2) {
+ void *tmp = *p_el1;
+ *p_el1 = *p_el2;
+ *p_el2 = tmp;
+}
+
+// expand heap exponentially to fit el_count elements
+void expand_heap(Heap *heap, size_t el_count) {
+ while (heap->capacity < el_count)
+ heap->capacity = (heap->capacity << 1) | 1;
+ heap->data = safe_realloc(heap->data, heap->capacity * sizeof(void *));
+}
+
+void heap_insert(Heap *heap, void *element) {
+ // expand heap if capacity is too small
+ if (heap->n_elements >= heap->capacity)
+ expand_heap(heap, heap->n_elements + 1);
+
+ // insert into the last position
+ size_t i = heap->n_elements;
+ heap->data[i] = safe_malloc(heap->element_size);
+ memcpy(heap->data[i], element, heap->element_size);
+ heap->n_elements++;
+
+ // heapify up
+ while (i > 0 && heap->compare(heap->data[i], heap->data[parent(i)])) {
+ swap(&heap->data[i], &heap->data[parent(i)]);
+ i = parent(i);
+ }
+}
+
+void *heap_top_get(Heap *heap) {
+ return heap->n_elements <= 0 ? NULL : heap->data[0];
+}
+
+void free_heap_element(Heap *heap, size_t index) {
+ if (heap->element_cleanup)
+ heap->element_cleanup(heap->data[index]);
+ free(heap->data[index]);
+}
+
+void heap_top_remove(Heap *heap) {
+ if (heap->n_elements <= 0)
+ return;
+
+ free_heap_element(heap, 0);
+
+ // move last element to root
+ heap->data[0] = heap->data[--heap->n_elements];
+
+ // heapify down
+ size_t i = 0;
+ int changed;
+ do {
+ size_t l = left(i);
+ size_t r = right(i);
+ size_t to_be_parent = i;
+
+ if (l < heap->n_elements && heap->compare(heap->data[l], heap->data[to_be_parent]))
+ to_be_parent = l;
+ if (r < heap->n_elements && heap->compare(heap->data[r], heap->data[to_be_parent]))
+ to_be_parent = r;
+
+ if ((changed = to_be_parent != i)) {
+ swap(&heap->data[to_be_parent], &heap->data[i]);
+ i = to_be_parent;
+ }
+ } while (changed);
+}
+
+void heap_free(Heap *heap) {
+ if (!heap)
+ return;
+ for (size_t i = 0; i < heap->n_elements; i++) {
+ free_heap_element(heap, i);
+ }
+ free(heap->data);
+ free(heap);
+}
diff --git a/analyzer/structures/heap.h b/analyzer/structures/heap.h
new file mode 100644
index 0000000..1ae72c0
--- /dev/null
+++ b/analyzer/structures/heap.h
@@ -0,0 +1,60 @@
+// structures/heap.h
+// a simple implementation of heap
+
+#include <stddef.h>
+
+#ifndef HEAP_H
+#define HEAP_H
+
+#define INIT_HEAP_DEPTH 7
+#define INIT_HEAP_CAPACITY (1 << INIT_HEAP_DEPTH) - 1
+
+typedef struct {
+ void **data;
+ size_t n_elements;
+ size_t element_size;
+ size_t capacity;
+ int (*compare)(void *element1, void *element2);
+ void (*element_cleanup)(void *element);
+} Heap;
+
+// create a new heap
+// params:
+// - element_size - size of the heap element
+// - compare - function for comparing elements
+// params:
+// - element1 - pointer to the first of the elements to compare
+// - element2 - pointer to the second of the elements to compare
+// returns:
+// truthy if element1 should be element2's parent (e.g. element1 > element2 for max heap), falsy otherwise
+// - element_cleanup - function for cleaning up any internal allocations inside element (not the element itself)
+// if it has no heap allocations, this should be NULL
+// params:
+// - element - pointer to the element to cleanup
+// returns:
+// pointer to the new heap
+extern Heap *heap_new(size_t element_size, int (*compare)(void *element1, void *element2), void (*element_cleanup)(void *element));
+
+// insert new element into the heap
+// params:
+// - heap - heap to insert to
+// - element - pointer to the element to insert
+extern void heap_insert(Heap *heap, void *element);
+
+// get the top element from the heap
+// params:
+// - heap - heap to get the top element from
+// returns:
+// pointer to the top element in the heap or NULL if heap is empty
+extern void *heap_top_get(Heap *heap);
+
+// remove the top element from the heap
+// params:
+// - heap - heap to remove the top element from
+extern void heap_top_remove(Heap *heap);
+
+// free the heap
+// - heap - heap to free
+extern void heap_free(Heap *heap);
+
+#endif
diff --git a/analyzer/utils/fs.c b/analyzer/utils/fs.c
index 76fb581..5e3eb1c 100644
--- a/analyzer/utils/fs.c
+++ b/analyzer/utils/fs.c
@@ -1,8 +1,9 @@
-#include "utils.h"
+#include "utils/fs.h"
-#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
#include <sys/stat.h>
-#include <unistd.h>
void get_home_dir(char *home_dir) {
strcpy(home_dir, getenv("HOME"));
@@ -35,9 +36,3 @@ void create_necessary_directories() {
mkdir(capture_dir, 0755);
mkdir(saves_dir, 0755);
}
-
-void write_file(char *path, void *contents, int contents_len, int flags, mode_t permissions) {
- int fd = open(path, flags, permissions);
- write(fd, contents, contents_len);
- close(fd);
-}
diff --git a/analyzer/utils/fs.h b/analyzer/utils/fs.h
index 6335799..8083ddc 100644
--- a/analyzer/utils/fs.h
+++ b/analyzer/utils/fs.h
@@ -1,8 +1,6 @@
// utils/fs.h
// functions for operating on the file system, i.e. creating directories, writing files, etc.
-#include <sys/types.h>
-
// get main directory used by the program
// params:
// - main_dir - buffer to save the directory to
@@ -20,12 +18,3 @@ extern void get_saves_dir(char *saves_dir);
// create directories that will be used by the program
extern void create_necessary_directories();
-
-// writes file to the specified path
-// params:
-// - path - path to write the file to
-// - contents - contents of the file
-// - contents_len - amount of bytes of contents to write
-// - flags - flags to open the file with
-// - permissions - permissions of the written file
-extern void write_file(char *path, void *contents, int contents_len, int flags, mode_t permissions);
diff --git a/analyzer/utils/memory.c b/analyzer/utils/memory.c
new file mode 100644
index 0000000..91c5f18
--- /dev/null
+++ b/analyzer/utils/memory.c
@@ -0,0 +1,62 @@
+#include "memory.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+void *safe_malloc(size_t size) {
+ for (size_t i = 0; i < MAX_ALLOC_ATTEMPTS; i++) {
+ void *p = malloc(size);
+ if (p)
+ return p;
+ if (i < MAX_ALLOC_ATTEMPTS - 1) {
+ fprintf(stderr, "Failed malloc. Retrying in 1s...\n");
+ sleep(1);
+ }
+ }
+
+ fprintf(stderr, "FATAL: Failed malloc %d times. Exiting...", MAX_ALLOC_ATTEMPTS);
+ exit(EXIT_FAILURE);
+}
+
+void *safe_realloc(void *ptr, size_t size) {
+ if (!ptr)
+ return safe_malloc(size);
+
+ for (size_t i = 0; i < MAX_ALLOC_ATTEMPTS; i++) {
+ void *p = realloc(ptr, size);
+ if (p)
+ return p;
+ if (i < MAX_ALLOC_ATTEMPTS - 1) {
+ fprintf(stderr, "Failed realloc. Retrying in 1s...\n");
+ sleep(1);
+ }
+ }
+
+ fprintf(stderr, "FATAL: Failed realloc %d times. Exiting...", MAX_ALLOC_ATTEMPTS);
+ exit(EXIT_FAILURE);
+}
+
+// expand an array
+// returns pointer to the expanded array
+void *array_expand(void *arr, size_t el_size, size_t *cap, size_t to_fit) {
+ while (*cap < to_fit)
+ *cap *= 2;
+ return safe_realloc(arr, *cap * el_size);
+}
+
+void *array_append(void *arr, size_t el_size, size_t *len, size_t *cap, void *val) {
+ if (*len >= *cap)
+ arr = array_expand(arr, el_size, cap, *len + 1);
+ memcpy((char *)arr + (*len)++ * el_size, val, el_size);
+ return arr;
+}
+
+void *array_concat(void *arr1, size_t el_size, size_t *len1, size_t *cap, void *arr2, size_t len2) {
+ if (*len1 + len2 > *cap)
+ arr1 = array_expand(arr1, el_size, cap, *len1 + len2);
+ memcpy((char *)arr1 + *len1 * el_size, arr2, len2 * el_size);
+ *len1 += len2;
+ return arr1;
+}
diff --git a/analyzer/utils/memory.h b/analyzer/utils/memory.h
new file mode 100644
index 0000000..0973623
--- /dev/null
+++ b/analyzer/utils/memory.h
@@ -0,0 +1,46 @@
+// utils/memory.h
+// helper functions for dealing with memory management
+
+#include <stddef.h>
+
+#define ARR_LEN(arr) (sizeof(arr) / sizeof(arr[0]))
+
+#define MAX_ALLOC_ATTEMPTS 3
+
+// tries malloc MAX_ALLOC_ATTEMPTS times, if all fail - exits the program
+// params:
+// - size - count of bytes to allocate
+// returns:
+// pointer to the allocated memory
+extern void *safe_malloc(size_t size);
+
+// tries realloc MAX_ALLOC_ATTEMPTS times, if all fail - exits the program
+// params:
+// - ptr - pointer to the memory to reallocate
+// - size - new size of the memory
+// returns:
+// pointer to the reallocated memory
+extern void *safe_realloc(void *ptr, size_t size);
+
+// append to array, expand if necessary
+// params:
+// - arr - array to append to
+// - el_size - size of an array element
+// - len - pointer to the length of the array
+// - cap - pointer to the capacity of the array
+// - val - pointer to the value to append to the array
+// returns:
+// new pointer to the array (different if it was realloced)
+extern void *array_append(void *arr, size_t el_size, size_t *len, size_t *cap, void *val);
+
+// concatenate 2 arrays, expand if necessary
+// params:
+// - arr1 - first array (destination)
+// - el_size - size of an array element
+// - len1 - pointer to the length of arr1
+// - cap - pointer to the capacity of arr1
+// - arr2 - second array
+// - len2 - length of arr2
+// returns:
+// new pointer to the array (different if it was realloced)
+extern void *array_concat(void *arr1, size_t el_size, size_t *len1, size_t *cap, void *arr2, size_t len2);
diff --git a/analyzer/utils/roman.c b/analyzer/utils/roman.c
new file mode 100644
index 0000000..4fbe5c0
--- /dev/null
+++ b/analyzer/utils/roman.c
@@ -0,0 +1,15 @@
+#include "utils/roman.h"
+
+#include <string.h>
+
+#include "utils/memory.h"
+
+void to_roman(char *buffer, int number) {
+ buffer[0] = '\0';
+ for (size_t i = 0; i < ARR_LEN(roman_symbols); i++) {
+ for (size_t j = 0; j < number / roman_symbols[i].num; j++) {
+ strcat(buffer, roman_symbols[i].symbol);
+ }
+ number %= roman_symbols[i].num;
+ }
+}
diff --git a/analyzer/utils/roman.h b/analyzer/utils/roman.h
new file mode 100644
index 0000000..cebd16e
--- /dev/null
+++ b/analyzer/utils/roman.h
@@ -0,0 +1,29 @@
+// utils/roman.h
+// functions for roman numbers
+
+typedef struct {
+ int num;
+ char *symbol;
+} RomanSymbol;
+
+static const RomanSymbol roman_symbols[] = {
+ {1000, "M"},
+ {900, "CM"},
+ {500, "D"},
+ {400, "CD"},
+ {100, "C"},
+ {90, "XC"},
+ {50, "L"},
+ {40, "XL"},
+ {10, "X"},
+ {9, "IX"},
+ {5, "V"},
+ {4, "IV"},
+ {1, "I"},
+};
+
+// convert a arabic number to roman number
+// params:
+// buffer - buffer to save the roman number to
+// number - arabic number to convert
+extern void to_roman(char *buffer, int number);
diff --git a/analyzer/utils/tcp.c b/analyzer/utils/tcp.c
new file mode 100644
index 0000000..6309e64
--- /dev/null
+++ b/analyzer/utils/tcp.c
@@ -0,0 +1,177 @@
+#include "utils/tcp.h"
+
+#include <netinet/if_ether.h>
+#include <netinet/ip.h>
+#include <netinet/tcp.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "utils/memory.h"
+#include "utils/tcp_seq_math.h"
+
+// initalize default TCP segment
+TCPSegment new_segment() {
+ // clang-format off
+ TCPSegment segment = {
+ .payload = NULL,
+ .len = 0,
+ .seq = 0,
+ .port = 0
+ };
+ // clang-format on
+ return segment;
+}
+
+// segments compare function to use with heap
+int segments_compare(void *segment1, void *segment2) {
+ TCPSegment *seg1 = segment1;
+ TCPSegment *seg2 = segment2;
+ return seq_before(seg1->seq, seg2->seq);
+}
+
+// segment cleanup function to use with heap
+void segment_cleanup(void *segment) {
+ TCPSegment *seg = segment;
+ free(seg->payload);
+}
+
+TCPSegment get_segment_from_packet(const byte_t *packet, const struct pcap_pkthdr *packet_data) {
+ TCPSegment segment = new_segment();
+
+ // get Ethernet header
+ const struct ether_header *eth_header = (const struct ether_header *)packet;
+ int eth_header_len = sizeof(struct ether_header);
+
+ // check if it's IP
+ if (ntohs(eth_header->ether_type) != ETHERTYPE_IP)
+ return segment;
+
+ // get IP header
+ const struct ip *ip_header = (const struct ip *)(packet + eth_header_len);
+ int ip_header_len = ip_header->ip_hl * 4;
+
+ // check if it's TCP
+ if (ip_header->ip_p != IPPROTO_TCP)
+ return segment;
+
+ // get TCP header
+ const struct tcphdr *tcp_header = (const struct tcphdr *)(packet + eth_header_len + ip_header_len);
+ int tcp_header_len = tcp_header->th_off * 4;
+
+ // get TCP sequence number and destination port
+ segment.seq = ntohl(tcp_header->seq);
+ segment.port = ntohs(tcp_header->th_dport);
+
+ // get payload
+ int total_header_len = eth_header_len + ip_header_len + tcp_header_len;
+ segment.len = packet_data->caplen - total_header_len;
+ segment.payload = safe_malloc(segment.len);
+ memcpy(segment.payload, packet + total_header_len, segment.len);
+ return segment;
+}
+
+TCPStream new_stream() {
+ // clang-format off
+ TCPStream stream = {
+ .data = safe_malloc(INIT_STREAM_CAPACITY),
+ .len = 0,
+ .capacity = INIT_STREAM_CAPACITY,
+ .seq = 0,
+ .port = 0,
+ .pending = NULL
+ };
+ // clang-format on
+ return stream;
+}
+
+void destroy_stream(TCPStream stream) {
+ free(stream.data);
+ heap_free(stream.pending);
+}
+
+void remove_parsed_data(TCPStream *stream, size_t parsed_len) {
+ memmove(stream->data, stream->data + parsed_len, stream->len - parsed_len);
+ stream->len -= parsed_len;
+ stream->seq += parsed_len;
+}
+
+// get the index of the first relevant byte in segment (its tcp sequence number is the one stream is expecting)
+// return the index of the first relevant byte in payload or -1 if segment isn't directly connected to stream
+int get_first_relevant_byte_index(TCPStream stream, TCPSegment segment) {
+ // expected tcp sequence numbers for stream and segment
+ uint32_t stream_expected = stream.seq + stream.len;
+ uint32_t segment_expected = segment.seq + segment.len;
+ if (seq_in_range_ie(stream_expected, segment.seq, segment_expected))
+ return stream_expected - segment.seq;
+ return -1;
+}
+
+// check if the segment is in acceptable range to the stream
+int in_acceptable_range(TCPStream stream, TCPSegment segment) {
+ // expected tcp sequence number for stream and max acceptable sequence number for segment
+ uint32_t stream_expected = stream.seq + stream.len;
+ uint32_t max_acceptable_seq = stream_expected + MAX_SEQ_OFFSET;
+ return seq_in_range_ei(segment.seq, stream_expected, max_acceptable_seq);
+}
+
+// add segment directly to stream
+void add_segment(TCPStream *stream, TCPSegment segment, size_t data_start) {
+ stream->data = array_concat(stream->data, 1, &stream->len, &stream->capacity, segment.payload + data_start, segment.len - data_start);
+}
+
+// add pending segments to the stream
+void add_pending_segments(TCPStream *stream) {
+ int top_changed = 1;
+ TCPSegment *top;
+ while (top_changed && (top = heap_top_get(stream->pending))) {
+ top_changed = 0;
+
+ // add pending segment it it became directly connected
+ int data_start = get_first_relevant_byte_index(*stream, *top);
+ if (data_start != -1) {
+ add_segment(stream, *top, data_start);
+ top_changed = 1;
+ }
+
+ // remove top if it was just added or no longer is in acceptable range (e.g. was contained within another one pending)
+ if (top_changed || !in_acceptable_range(*stream, *top)) {
+ heap_top_remove(stream->pending);
+ }
+ }
+}
+
+int handle_segment(TCPStream *stream, TCPSegment segment) {
+ // add the segment if port is different (new connection)
+ if (stream->port != segment.port) {
+ // create new heap for new pending segments
+ heap_free(stream->pending);
+ stream->pending = heap_new(sizeof(TCPSegment), segments_compare, segment_cleanup);
+
+ stream->len = 0;
+ stream->seq = segment.seq;
+ stream->port = segment.port;
+
+ add_segment(stream, segment, 0);
+ free(segment.payload);
+ return 1;
+ }
+
+ // add segment directly to the stream if it's directly connected
+ int data_start = get_first_relevant_byte_index(*stream, segment);
+ if (data_start != -1) {
+ add_segment(stream, segment, data_start);
+ free(segment.payload);
+ add_pending_segments(stream);
+ return 1;
+ }
+
+ // add to pending if segment is in acceptable range:
+ if (in_acceptable_range(*stream, segment)) {
+ heap_insert(stream->pending, &segment);
+ return 0;
+ }
+
+ // if neither, discard entirely
+ free(segment.payload);
+ return 0;
+}
diff --git a/analyzer/utils/tcp.h b/analyzer/utils/tcp.h
new file mode 100644
index 0000000..13cbb15
--- /dev/null
+++ b/analyzer/utils/tcp.h
@@ -0,0 +1,69 @@
+// utils/tcp.h
+// functions for handling TCP streams/segments
+
+#include <pcap.h>
+
+#include "structures/heap.h"
+
+#ifndef TCP_H
+#define TCP_H
+
+typedef unsigned char byte_t;
+
+// struct to persistently store TCP segments
+// payload field is malloced and needs to be freed
+typedef struct {
+ byte_t *payload; // payload in the segment
+ size_t len; // length of the payload
+ uint32_t seq; // TCP sequence number of the segment
+ uint16_t port; // destination (client) port of connection the segment was received from
+} TCPSegment;
+
+// get TCP segment from captured packet
+// params:
+// - packet - captured packet
+// - packet_data - metadata about packet
+// returns:
+// TCPSegment from packet, payload is NULL if the received packet was incorrect
+extern TCPSegment get_segment_from_packet(const byte_t *packet, const struct pcap_pkthdr *packet_data);
+
+// struct that stores all the information about stream
+typedef struct {
+ byte_t *data; // bytes in the stream
+ size_t len; // length of the stream
+ size_t capacity; // capacity of the data buffer
+ uint32_t seq; // TCP sequence number of the first byte in the stream
+ uint16_t port; // destination (client) port of current analyzed connection
+ Heap *pending; // min heap of segments yet to be added to stream (sorted by TCP sequence number)
+} TCPStream;
+
+#define INIT_STREAM_CAPACITY 256
+
+// initialize new stream
+// returns:
+// newly created TCPStream structure with initial values
+extern TCPStream new_stream();
+
+// cleanup memory allocations within stream
+// params:
+// stream - stream to destroy
+extern void destroy_stream(TCPStream stream);
+
+// remove parsed data from the stream
+// params:
+// - stream - pointer to the stream to remove a part of
+// - parsed_len - count of n first bytes of the stream that were parsed
+extern void remove_parsed_data(TCPStream *stream, size_t parsed_len);
+
+// max difference between expected TCP seq and received TCP seq to still accept it into pending
+#define MAX_SEQ_OFFSET 100000
+
+// add TCP segment to stream, pending or completely discard it
+// params:
+// - stream - pointer to the current stream
+// - segment - segment to handle
+// returns:
+// 1 if the segment was added to the stream, 0 if it was discarded or saved to pending
+extern int handle_segment(TCPStream *stream, TCPSegment segment);
+
+#endif
diff --git a/analyzer/utils/tcp_seq_math.c b/analyzer/utils/tcp_seq_math.c
new file mode 100644
index 0000000..2cc6e3f
--- /dev/null
+++ b/analyzer/utils/tcp_seq_math.c
@@ -0,0 +1,41 @@
+#include "utils/tcp_seq_math.h"
+
+int seq_after(uint32_t a, uint32_t b) {
+ return (int32_t)(a - b) > 0;
+}
+
+int seq_before(uint32_t a, uint32_t b) {
+ return (int32_t)(a - b) < 0;
+}
+
+int seq_after_eq(uint32_t a, uint32_t b) {
+ return (int32_t)(a - b) >= 0;
+}
+
+int seq_before_eq(uint32_t a, uint32_t b) {
+ return (int32_t)(a - b) <= 0;
+}
+
+int seq_in_range_ii(uint32_t value, uint32_t range_start, uint32_t range_end) {
+ int after_eq = seq_after_eq(value, range_start);
+ int before_eq = seq_before_eq(value, range_end);
+ return after_eq && before_eq;
+}
+
+int seq_in_range_ee(uint32_t value, uint32_t range_start, uint32_t range_end) {
+ int after = seq_after(value, range_start);
+ int before = seq_before(value, range_end);
+ return after && before;
+}
+
+int seq_in_range_ie(uint32_t value, uint32_t range_start, uint32_t range_end) {
+ int after_eq = seq_after_eq(value, range_start);
+ int before = seq_before(value, range_end);
+ return after_eq && before;
+}
+
+int seq_in_range_ei(uint32_t value, uint32_t range_start, uint32_t range_end) {
+ int after = seq_after(value, range_start);
+ int before_eq = seq_before_eq(value, range_end);
+ return after && before_eq;
+}
diff --git a/analyzer/utils/tcp_seq_math.h b/analyzer/utils/tcp_seq_math.h
new file mode 100644
index 0000000..c4da5f9
--- /dev/null
+++ b/analyzer/utils/tcp_seq_math.h
@@ -0,0 +1,28 @@
+// utils/tcp_seq_math.h
+// helper functions for math regarding tcp sequence numbers which correctly handle overflow
+
+#include <stdint.h>
+
+// check if a is after b
+extern int seq_after(uint32_t a, uint32_t b);
+
+// check if a is before b
+extern int seq_before(uint32_t a, uint32_t b);
+
+// check if a is after or equal to b
+extern int seq_after_eq(uint32_t a, uint32_t b);
+
+// check if a is before or equal to b
+extern int seq_before_eq(uint32_t a, uint32_t b);
+
+// check if value is in [range_start; range_end]
+extern int seq_in_range_ii(uint32_t value, uint32_t range_start, uint32_t range_end);
+
+// check if value is in (range_start; range_end)
+extern int seq_in_range_ee(uint32_t value, uint32_t range_start, uint32_t range_end);
+
+// check if value is in [range_start; range_end)
+extern int seq_in_range_ie(uint32_t value, uint32_t range_start, uint32_t range_end);
+
+// check if value is in (range_start; range_end]
+extern int seq_in_range_ei(uint32_t value, uint32_t range_start, uint32_t range_end);
diff --git a/analyzer/utils/time.c b/analyzer/utils/time.c
index 72c0bc7..d95da65 100644
--- a/analyzer/utils/time.c
+++ b/analyzer/utils/time.c
@@ -1,4 +1,4 @@
-#include "time.h"
+#include "utils/time.h"
#include <stdio.h>
#include <time.h>
diff --git a/csv/artifact_sizes.csv b/csv/artifact_sizes.csv
new file mode 100644
index 0000000..a9af916
--- /dev/null
+++ b/csv/artifact_sizes.csv
@@ -0,0 +1,5 @@
+id,artifact_size
+1,Sub
+2,Bi
+3,Magni
+4,Arcy
diff --git a/csv/items.csv b/csv/items.csv
new file mode 100644
index 0000000..0265384
--- /dev/null
+++ b/csv/items.csv
@@ -0,0 +1,6 @@
+id,item_name
+1222,Daimonit
+1219,Trop trwogi
+986,KrysztaƂ czasu
+703,Glif arcymistrzowski
+697,Glif bakalarski
diff --git a/csv/orbs.csv b/csv/orbs.csv
new file mode 100644
index 0000000..02dfde4
--- /dev/null
+++ b/csv/orbs.csv
@@ -0,0 +1,3 @@
+id,orb_name
+21,Ha'at
+12,Se'at
diff --git a/csv/saturations.csv b/csv/saturations.csv
new file mode 100644
index 0000000..35797cb
--- /dev/null
+++ b/csv/saturations.csv
@@ -0,0 +1,8 @@
+id,saturation_type
+1,Gherudu
+2,Velhar
+3,Yantrei
+4,Hegun
+5,Solnai
+6,Farao
+7,Kosani
diff --git a/csv_table_gen.c b/csv_table_gen.c
new file mode 100644
index 0000000..ae80b78
--- /dev/null
+++ b/csv_table_gen.c
@@ -0,0 +1,125 @@
+#include <dirent.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/stat.h>
+
+#define EXT ".csv"
+#define EXT_LEN strlen(".csv")
+#define CSV_PATH "csv"
+#define OUT_PATH "analyzer/" CSV_PATH
+
+#define BUFFER_SIZE 256
+#define MAX_ARR_SIZE 4096
+
+// read values from input csv and save them to buffers
+// returns max_index of the array
+int read_csv(char *path, char *from, char *to, char from_to_array[][BUFFER_SIZE]) {
+ FILE *fp = fopen(path, "r");
+
+ // read field names
+ fscanf(fp, "%[^,],%[^\n]", from, to);
+
+ // read values
+ size_t max_index = 0;
+ size_t val1;
+ char val2[BUFFER_SIZE];
+ while (fscanf(fp, "%zu,%[^\n]", &val1, val2) == 2) {
+ strcpy(from_to_array[val1], val2);
+ if (val1 > max_index)
+ max_index = val1;
+ }
+
+ fclose(fp);
+
+ return max_index;
+}
+
+// create output .h file
+void create_h(char *path, char *from, char *to) {
+ FILE *fp = fopen(path, "w");
+
+ // write comments and function prototype
+ fprintf(fp, "// %s\n", strstr(path, CSV_PATH));
+ fprintf(fp, "// translate %s to %s - generated from csv\n\n", from, to);
+ fprintf(fp, "// get %s from %s\n", to, from);
+ fprintf(fp, "// params:\n");
+ fprintf(fp, "// - %s - %s to translate from\n", from, from);
+ fprintf(fp, "// returns:\n");
+ fprintf(fp, "// %s corresponding to %s\n", to, from);
+ fprintf(fp, "extern const char *get_%s_from_%s(int %s);", to, from, from);
+
+ fclose(fp);
+}
+
+// create output .c file
+void create_c(char *path, char *h_path, char *from, char *to, char from_to_array[][BUFFER_SIZE], size_t max_index) {
+ FILE *fp = fopen(path, "w");
+
+ char arr_name[BUFFER_SIZE], arr_len_name[BUFFER_SIZE];
+ sprintf(arr_name, "%s_to_%s", from, to);
+ sprintf(arr_len_name, "%s_to_%s_len", from, to);
+
+ // write includes and the beginning of array
+ fprintf(fp, "#include \"%s\"\n\n", strstr(h_path, CSV_PATH));
+ fprintf(fp, "#include <stdio.h>\n\n");
+ fprintf(fp, "static const char *%s[] = {\n", arr_name);
+
+ // write values
+ for (size_t i = 0; i <= max_index; i++) {
+ if (strlen(from_to_array[i]) > 0)
+ fprintf(fp, " \"%s\",\n", from_to_array[i]);
+ else
+ fprintf(fp, " \"%zu\",\n", i);
+ }
+
+ // write the end of array and len
+ fprintf(fp, "};\n");
+ fprintf(fp, "int %s = %zu;\n\n", arr_len_name, max_index + 1);
+
+ // write the getter
+ fprintf(fp, "const char *get_%s_from_%s(int %s) {\n", to, from, from);
+ fprintf(fp, " if (%s >= 0 && %s < %s)\n", from, from, arr_len_name);
+ fprintf(fp, " return %s[%s];\n", arr_name, from);
+ fprintf(fp, " fprintf(stderr, \"%s = %%d is outside of table range [0; %%d]\\n\", %s, %s - 1);\n", from, from, arr_len_name);
+ fprintf(fp, " return \"null\";\n");
+ fprintf(fp, "}");
+
+ fclose(fp);
+}
+
+void create_table(char *file_name) {
+ // read from input file
+ char input_path[BUFFER_SIZE];
+ sprintf(input_path, "%s/%s", CSV_PATH, file_name);
+ char from[BUFFER_SIZE], to[BUFFER_SIZE];
+ char from_to_array[MAX_ARR_SIZE][BUFFER_SIZE] = {0};
+ size_t max_index = read_csv(input_path, from, to, from_to_array);
+
+ // create output .h file
+ char h_path[BUFFER_SIZE];
+ sprintf(h_path, "%s/%s_to_%s.h", OUT_PATH, from, to);
+ create_h(h_path, from, to);
+
+ // create output .c file
+ char c_path[BUFFER_SIZE];
+ sprintf(c_path, "%s/%s_to_%s.c", OUT_PATH, from, to);
+ create_c(c_path, h_path, from, to, from_to_array, max_index);
+}
+
+int main() {
+ // make directory for output files
+ mkdir(OUT_PATH, 0755);
+
+ DIR *csv_dir = opendir(CSV_PATH);
+ struct dirent *entry;
+ while ((entry = readdir(csv_dir))) {
+ char *file_name = entry->d_name;
+ size_t file_name_len = strlen(file_name);
+ // create lookup tables for each *.csv file
+ if (file_name_len > EXT_LEN && !strcmp(file_name + file_name_len - EXT_LEN, EXT))
+ create_table(entry->d_name);
+ }
+ closedir(csv_dir);
+
+ return 0;
+}
diff --git a/main.c b/main.c
index 456b590..38dfc36 100644
--- a/main.c
+++ b/main.c
@@ -1,61 +1,18 @@
-#include <stdio.h>
-#include <string.h>
-
-#include "analyzer/capture.h"
-#include "analyzer/utils/fs.h"
-
-// prints help
-void print_help() {
- printf("A bare-bones analyzer for the game Broken Ranks\n");
- printf("\nUsage:\n");
- printf(" branal - start live analysis\n");
- printf(" branal (l | live) - start live analysis (same as above)\n");
- printf(" branal (c | capture) [file] - start capturing packets to [file].\n");
- printf(" If [file] not given - captures to $HOME/.local/share/branal/captures\n");
- printf(" branal (f | file) <file> - analyze the specified <file>\n");
- printf(" branal (h | help) - display this help\n");
-}
-
-// starts live analyzer mode
-void start_live_analyzer() {
- // TODO: implement
- printf("Not implemented\n");
-}
-
-// analyzes the specified file
-// params:
-// - file_path - path to the .pcap file to be analyzed
-void analyze_file(char *file_path) {
- // TODO: implement
- printf("Not implemented\n");
-}
+#include "commands.h"
+#include "utils/fs.h"
int main(int argc, char *argv[]) {
create_necessary_directories();
- // start live analysis if no arguments provided
- if (argc < 2) {
- start_live_analyzer();
- return 0;
- }
+ // start first command if no arguments provided
+ if (argc < 2)
+ return commands[0].handler(argc, argv);
- if (strcmp("l", argv[1]) == 0 || strcmp("live", argv[1]) == 0) {
- start_live_analyzer();
- }
- else if (strcmp("c", argv[1]) == 0 || strcmp("capture", argv[1]) == 0) {
- capturing_mode(argc >= 3 ? argv[2] : NULL);
- }
- else if (strcmp("h", argv[1]) == 0 || strcmp("help", argv[1]) == 0) {
- print_help();
- }
- else if (argc >= 3 && (strcmp("f", argv[1]) == 0 || strcmp("file", argv[1]) == 0)) {
- analyze_file(argv[2]);
- }
- else {
- printf("Command not recognized\n");
- printf("Use \"branal (h | help)\" for help\n");
- return 1;
+ for (size_t i = 0; i < commands_count; i++) {
+ if (is_command(commands[i], argv[1]))
+ return commands[i].handler(argc, argv);
}
- return 0;
+ command_not_recognized();
+ return 1;
}