| /* Minecraft Interprocess Management Client |
| * Copyright 2015-2020 Rivoreo |
| * |
| * This program is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License as published by the |
| * Free Software Foundation, either version 3 of the License, or (at your |
| * option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for |
| * more details. |
| */ |
| |
| #include <unistd.h> |
| #include <termios.h> |
| #include <string.h> |
| #include <stdarg.h> |
| #include <stdlib.h> |
| #include <stdio.h> |
| #include "packet.h" |
| |
| enum colum { |
| ENTITYSTAT_COLUM_WORLD, |
| ENTITYSTAT_COLUM_ID, |
| ENTITYSTAT_COLUM_TYPE, |
| ENTITYSTAT_COLUM_NAME, |
| ENTITYSTAT_COLUM_LOCATION, |
| ENTITYSTAT_COLUM_ROTATION, |
| ENTITYSTAT_COLUM_STATE, |
| ENTITYSTAT_COLUM_WIDTH, |
| ENTITYSTAT_COLUM_HEIGHT, |
| ENTITYSTAT_COLUM_CHUNK, |
| ENTITYSTAT_COLUM_UUID, |
| ENTITYSTAT_COLUM_MAX_HEALTH, |
| ENTITYSTAT_COLUM_HEALTH, |
| ENTITYSTAT_COLUM_PERCENTAGE_HEALTH, |
| ENTITYSTAT_COLUM_IDLE_TIME, |
| ENTITYSTAT_COLUM_ATTACK_TARGET_ID, |
| }; |
| |
| static const struct colum_name_header_map_entry { |
| enum colum id; |
| const char *name; |
| const char *default_header_name; |
| } colum_name_header_map[] = { |
| { ENTITYSTAT_COLUM_WORLD, "world", "WORLD" }, |
| { ENTITYSTAT_COLUM_ID, "id", "ENTITY-ID" }, |
| { ENTITYSTAT_COLUM_TYPE, "type", "TYPE" }, |
| { ENTITYSTAT_COLUM_NAME, "name", "NAME" }, |
| { ENTITYSTAT_COLUM_LOCATION, "location", "LOCATION" }, |
| { ENTITYSTAT_COLUM_ROTATION, "rotation", "ROTATION" }, |
| { ENTITYSTAT_COLUM_STATE, "state", "STATE" }, |
| { ENTITYSTAT_COLUM_WIDTH, "width", "WIDTH" }, |
| { ENTITYSTAT_COLUM_HEIGHT, "height", "HEIGHT" }, |
| { ENTITYSTAT_COLUM_CHUNK, "chunk", "CHUNK" }, |
| { ENTITYSTAT_COLUM_UUID, "uuid", "UUID" }, |
| { ENTITYSTAT_COLUM_MAX_HEALTH, "maxhealth", "MAX-HEALTH" }, |
| { ENTITYSTAT_COLUM_HEALTH, "health", "HEALTH" }, |
| { ENTITYSTAT_COLUM_PERCENTAGE_HEALTH, "phealth", "%HEALTH" }, |
| { ENTITYSTAT_COLUM_IDLE_TIME, "idletime", "IDLE-TIME" }, |
| { ENTITYSTAT_COLUM_ATTACK_TARGET_ID, "attacktarget", "ATTACK-TARGET-ID" }, |
| { -1, NULL, NULL } |
| }; |
| |
| struct format_configure_node { |
| enum colum colum; |
| const char *header_name; |
| }; |
| static const struct format_configure_node default_format_configuration[] = { |
| { ENTITYSTAT_COLUM_WORLD }, |
| { ENTITYSTAT_COLUM_ID }, |
| { ENTITYSTAT_COLUM_LOCATION }, |
| { ENTITYSTAT_COLUM_TYPE }, |
| { ENTITYSTAT_COLUM_NAME }, |
| { -1 } |
| }; |
| #define DEFAULT_COLUM_COUNT 5 |
| static struct format_configure_node *format_configuration; |
| static unsigned int custom_colum_count; |
| |
| static void add_colum(struct format_configure_node config) { |
| format_configuration = realloc(format_configuration, sizeof(struct format_configure_node) * (custom_colum_count + 2)); |
| if(!format_configuration) { |
| fputs("Out of memory\n", stderr); |
| exit(1); |
| } |
| format_configuration[custom_colum_count++] = config; |
| format_configuration[custom_colum_count] = (struct format_configure_node){ -1, NULL }; |
| } |
| |
| static enum colum get_colum(const char *name) { |
| const struct colum_name_header_map_entry *e = colum_name_header_map; |
| while(e->id != (enum colum)-1) { |
| if(strcmp(e->name, name) == 0) return e->id; |
| e++; |
| } |
| return -1; |
| } |
| |
| int entitystat_set_output_format(const char *format) { |
| if(!*format) { |
| fputs("output format specification cannot be empty\n", stderr); |
| return -1; |
| } |
| char *colum_name; |
| enum colum colum; |
| const char *p = format; |
| while(1) switch(*p) { |
| case 0: |
| case '=': |
| case ',': |
| colum_name = malloc(p - format + 1); |
| if(!colum_name) { |
| fputs("Out of memory\n", stderr); |
| exit(1); |
| } |
| memcpy(colum_name, format, p - format); |
| colum_name[p - format] = 0; |
| colum = get_colum(colum_name); |
| if(colum == (enum colum)-1) { |
| fprintf(stderr, "Unknown colum '%s'\n", colum_name); |
| return -1; |
| } |
| add_colum((struct format_configure_node){ colum, *p == '=' ? p + 1 : NULL }); |
| if(*p != ',') return 0; |
| format = p + 1; |
| // Fallthrough |
| default: |
| p++; |
| break; |
| } |
| } |
| |
| static char *xasprintf(const char *format, ...) { |
| char *r; |
| va_list va; |
| va_start(va, format); |
| if(vasprintf(&r, format, va) < 0) { |
| perror("vasprintf"); |
| exit(1); |
| } |
| va_end(va); |
| return r; |
| } |
| |
| static char *uuid_to_string(const unsigned char *p) { |
| return xasprintf("%02hhx%02hhx%02hhx%02hhx-%02hhx%02hhx-%02hhx%02hhx-%02hhx%02hhx-%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx", |
| p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], p[9], p[10], p[11], p[12], p[13], p[14], p[15]); |
| } |
| |
| static void print_spaces(size_t count) { |
| while(count > 0) { |
| putchar(' '); |
| count--; |
| } |
| } |
| |
| void entitystat_print(struct entity **entities, unsigned int count, int wide) { |
| unsigned int colum_i, row_i, e_i; |
| int should_print_header = 0; |
| const struct format_configure_node *config; |
| unsigned int colum_count; |
| if(format_configuration) { |
| config = format_configuration; |
| colum_count = custom_colum_count; |
| for(colum_i = 0; colum_i < colum_count; colum_i++) { |
| const char *name = config[colum_i].header_name; |
| if(!name || *name) { |
| should_print_header = 1; |
| break; |
| } |
| } |
| } else { |
| config = default_format_configuration; |
| colum_count = DEFAULT_COLUM_COUNT; |
| should_print_header = 1; |
| } |
| |
| const char *output[should_print_header + count][colum_count]; |
| if(should_print_header) { |
| for(colum_i = 0; colum_i < colum_count; colum_i++) { |
| const char *name = config[colum_i].header_name; |
| if(!name) { |
| const struct colum_name_header_map_entry *e = colum_name_header_map; |
| while(e->id != (enum colum)-1) { |
| if(e->id == config[colum_i].colum) { |
| name = e->default_header_name; |
| break; |
| } |
| e++; |
| } |
| //assert(name != NULL); |
| } |
| output[0][colum_i] = name; |
| } |
| row_i = 1; |
| } else { |
| row_i = 0; |
| } |
| for(e_i = 0; e_i < count; e_i++) { |
| const struct entity *entity = entities[e_i]; |
| for(colum_i = 0; colum_i < colum_count; colum_i++) { |
| switch(config[colum_i].colum) { |
| case ENTITYSTAT_COLUM_WORLD: |
| output[row_i][colum_i] = (const char *)(entity->data + entity->world_name_offset); |
| break; |
| case ENTITYSTAT_COLUM_ID: |
| output[row_i][colum_i] = xasprintf("%u", (unsigned int)entity->entity_id); |
| break; |
| case ENTITYSTAT_COLUM_TYPE: |
| output[row_i][colum_i] = (const char *)(entity->data + entity->entity_type_name_offset); |
| break; |
| case ENTITYSTAT_COLUM_NAME: |
| output[row_i][colum_i] = (const char *)(entity->data + entity->display_name_offset); |
| break; |
| case ENTITYSTAT_COLUM_LOCATION: |
| output[row_i][colum_i] = xasprintf("(%g,%g,%g)", entity->location_x, entity->location_y, entity->location_z); |
| break; |
| case ENTITYSTAT_COLUM_ROTATION: |
| output[row_i][colum_i] = xasprintf("(%g,%g)", (double)entity->rotation_yaw, (double)entity->rotation_pitch); |
| break; |
| case ENTITYSTAT_COLUM_STATE: |
| output[row_i][colum_i] = xasprintf("%c%c", entity->is_on_ground ? 'G' : '-', entity->is_dead ? 'D' : '-'); |
| break; |
| case ENTITYSTAT_COLUM_WIDTH: |
| output[row_i][colum_i] = xasprintf("%g", (double)entity->width); |
| break; |
| case ENTITYSTAT_COLUM_HEIGHT: |
| output[row_i][colum_i] = xasprintf("%g", (double)entity->height); |
| break; |
| case ENTITYSTAT_COLUM_CHUNK: |
| output[row_i][colum_i] = xasprintf("(%d,%d,%d)", (int)entity->chunk_x, (int)entity->chunk_y, (int)entity->chunk_z); |
| break; |
| case ENTITYSTAT_COLUM_UUID: |
| output[row_i][colum_i] = uuid_to_string((const unsigned char *)&entity->uuid_most_sig); |
| break; |
| case ENTITYSTAT_COLUM_MAX_HEALTH: |
| output[row_i][colum_i] = entity->is_living_entity ? |
| xasprintf("%g", (double)entity->max_health) : "-"; |
| break; |
| case ENTITYSTAT_COLUM_HEALTH: |
| output[row_i][colum_i] = entity->is_living_entity ? |
| xasprintf("%g", (double)entity->health) : "-"; |
| break; |
| case ENTITYSTAT_COLUM_PERCENTAGE_HEALTH: |
| output[row_i][colum_i] = entity->is_living_entity ? |
| xasprintf("%g%%", (double)entity->health / entity->max_health * 100) : "-"; |
| break; |
| case ENTITYSTAT_COLUM_IDLE_TIME: |
| output[row_i][colum_i] = entity->is_living_entity ? |
| xasprintf("%u", (unsigned int)entity->idle_time) : "-"; |
| break; |
| case ENTITYSTAT_COLUM_ATTACK_TARGET_ID: |
| output[row_i][colum_i] = entity->is_living_entity && entity->attack_target_id != (uint32_t)-1 ? |
| xasprintf("%u", (unsigned int)entity->attack_target_id) : "-"; |
| break; |
| } |
| } |
| row_i++; |
| } |
| |
| unsigned int colum_widths[colum_count]; |
| unsigned int colum_width_except_last = 0; |
| for(colum_i = 0; colum_i < colum_count; colum_i++) { |
| unsigned int *width = colum_widths + colum_i; |
| *width = 0; |
| for(row_i = 0; row_i < should_print_header + count; row_i++) { |
| size_t len = strlen(output[row_i][colum_i]) + 1; |
| if(len > *width) *width = len; |
| } |
| if(colum_i + 1 < colum_count) colum_width_except_last += *width; |
| } |
| |
| unsigned int max_width = 0; |
| if(!wide && config[colum_count - 1].colum == ENTITYSTAT_COLUM_NAME && isatty(STDOUT_FILENO)) { |
| struct winsize ws; |
| if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != -1) { |
| max_width = ws.ws_col; |
| } |
| } |
| |
| for(row_i = 0; row_i < should_print_header + count; row_i++) { |
| for(colum_i = 0; colum_i < colum_count; colum_i++) { |
| int is_last = colum_i + 1 == colum_count; |
| size_t len = strlen(output[row_i][colum_i]); |
| if(is_last && max_width && len > 6 && colum_width_except_last + len > max_width) { |
| if(max_width < colum_width_except_last || max_width - colum_width_except_last < 6) len = 6; |
| else len = max_width - colum_width_except_last; |
| } |
| fwrite(output[row_i][colum_i], len, 1, stdout); |
| if(!is_last) print_spaces(colum_widths[colum_i] - len); |
| } |
| putchar('\n'); |
| } |
| } |