blob: 164f9cab58727115498d7548a43d6bf143579e60 [file] [log] [blame] [raw]
/* 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.
*/
#define _GNU_SOURCE 1
#include <sys/ioctl.h>
#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_TICKS,
ENTITYSTAT_COLUM_MAX_HEALTH,
ENTITYSTAT_COLUM_HEALTH,
ENTITYSTAT_COLUM_PERCENTAGE_HEALTH,
ENTITYSTAT_COLUM_IDLE_TIME,
ENTITYSTAT_COLUM_ATTACK_TARGET_ID,
ENTITYSTAT_COLUM_GROWING_AGE,
ENTITYSTAT_COLUM_ITEM_AGE,
ENTITYSTAT_COLUM_ITEM_PICKUP_DELAY,
ENTITYSTAT_COLUM_OWNER,
ENTITYSTAT_COLUM_THROWER,
ENTITYSTAT_COLUM_ITEM_DAMAGE,
ENTITYSTAT_COLUM_ITEM_LIFESPAN,
ENTITYSTAT_COLUM_ITEM_COUNT,
};
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_TICKS, "ticks", "TICKS" },
{ 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" },
{ ENTITYSTAT_COLUM_GROWING_AGE, "growingage", "GROWING-AGE" },
{ ENTITYSTAT_COLUM_ITEM_AGE, "itemage", "ITEM-AGE" },
{ ENTITYSTAT_COLUM_ITEM_PICKUP_DELAY, "itempickupdelay", "PICKUP-DELAY" },
{ ENTITYSTAT_COLUM_OWNER, "owner", "OWNER" },
{ ENTITYSTAT_COLUM_THROWER, "thrower", "THROWER" },
{ ENTITYSTAT_COLUM_ITEM_DAMAGE, "itemdamage", "ITEM-DAMAGE" },
{ ENTITYSTAT_COLUM_ITEM_LIFESPAN, "itemlifespan", "ITEM-LIFESPAN" },
{ ENTITYSTAT_COLUM_ITEM_COUNT, "itemcount", "ITEM-COUNT" },
{ -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_column(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_column(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 *column_name;
enum colum colum;
const char *p = format;
while(1) switch(*p) {
case 0:
case '=':
case ',':
column_name = malloc(p - format + 1);
if(!column_name) {
fputs("Out of memory\n", stderr);
exit(1);
}
memcpy(column_name, format, p - format);
column_name[p - format] = 0;
colum = get_column(column_name);
if(colum == (enum colum)-1) {
fprintf(stderr, "Unknown column '%s'\n", column_name);
return -1;
}
add_column((struct format_configure_node){ colum, *p == '=' ? p + 1 : NULL });
if(*p != ',') return 0;
format = p + 1;
// Fallthrough
default:
p++;
break;
}
}
static struct sort_key {
enum colum colum;
int direction;
} *sort_configuration;
static unsigned int sort_key_count;
static void add_sort_key(struct sort_key key) {
sort_configuration = realloc(sort_configuration, sizeof(struct sort_key) * (sort_key_count + 1));
if(!sort_configuration) {
fputs("Out of memory\n", stderr);
exit(1);
}
sort_configuration[sort_key_count++] = key;
}
int entitystat_set_sort(const char *spec) {
if(!*spec) {
fputs("sort specification cannot be empty\n", stderr);
return -1;
}
char *column_name;
enum colum colum;
int direction = 1;
const char *p = spec;
while(1) switch(*p) {
case '+':
case '-':
if(p != spec) {
fprintf(stderr, "Invalid sign '%c' in middle of a sort key\n", *p);
return -1;
}
direction = *p == '+' ? 1 : -1;
spec++;
p++;
break;
case 0:
case ',':
column_name = malloc(p - spec + 1);
if(!column_name) {
fputs("Out of memory\n", stderr);
exit(1);
}
memcpy(column_name, spec, p - spec);
column_name[p - spec] = 0;
colum = get_column(column_name);
if(colum == (enum colum)-1) {
fprintf(stderr, "Unknown column '%s'\n", column_name);
return -1;
}
free(column_name);
add_sort_key((struct sort_key){ colum, direction });
direction = 1;
if(*p != ',') return 0;
spec = 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--;
}
}
static int compare_entities(const void *v1, const void *v2) {
const struct entity *entity1 = *(const struct entity **)v1;
const struct entity *entity2 = *(const struct entity **)v2;
unsigned int i = 0;
do {
int diff;
switch(sort_configuration[i].colum) {
case ENTITYSTAT_COLUM_WORLD:
diff = strcmp((const char *)entity1->data + entity1->world_name_offset,
(const char *)entity2->data + entity2->world_name_offset);
break;
case ENTITYSTAT_COLUM_ID:
diff = entity1->entity_id - entity2->entity_id;
break;
case ENTITYSTAT_COLUM_TYPE:
diff = strcmp((const char *)entity1->data + entity1->entity_type_name_offset,
(const char *)entity2->data + entity2->entity_type_name_offset);
break;
case ENTITYSTAT_COLUM_NAME:
diff = strcmp((const char *)entity1->data + entity1->display_name_offset,
(const char *)entity2->data + entity2->display_name_offset);
break;
case ENTITYSTAT_COLUM_LOCATION:
/*
diff = (entity1->location_x - entity2->location_x) * 10 +
(entity1->location_y - entity2->location_y) * 10 +
(entity1->location_z - entity2->location_z) * 10;
*/
/*
diff = (entity1->location_x < entity2->location_x ? -100 : 100) +
(entity1->location_y < entity2->location_y ? -10 : 10) +
(entity1->location_z < entity2->location_z ? -1 : 1);
*/
if(entity1->location_x < entity2->location_x) diff = -1;
else if(entity1->location_x > entity2->location_x) diff = 1;
else if(entity1->location_y < entity2->location_y) diff = -1;
else if(entity1->location_y > entity2->location_y) diff = 1;
else if(entity1->location_z < entity2->location_z) diff = -1;
else if(entity1->location_z > entity2->location_z) diff = 1;
else diff = 0;
break;
case ENTITYSTAT_COLUM_ROTATION:
if(entity1->rotation_yaw < entity2->rotation_yaw) diff = -1;
else if(entity1->rotation_yaw > entity2->rotation_yaw) diff = 1;
else if(entity1->rotation_pitch < entity2->rotation_pitch) diff = -1;
else if(entity1->rotation_pitch > entity2->rotation_pitch) diff = 1;
else diff = 0;
break;
case ENTITYSTAT_COLUM_STATE:
diff = (entity1->is_dead - entity2->is_dead) * 10 + (entity1->is_on_ground - entity2->is_on_ground);
break;
case ENTITYSTAT_COLUM_WIDTH:
if(entity1->width < entity2->width) diff = -1;
else if(entity1->width > entity2->width) diff = 1;
else diff = 0;
break;
case ENTITYSTAT_COLUM_HEIGHT:
if(entity1->height < entity2->height) diff = -1;
else if(entity1->height > entity2->height) diff = 1;
else diff = 0;
break;
case ENTITYSTAT_COLUM_CHUNK:
diff = (entity1->chunk_x - entity2->chunk_x) + (entity1->chunk_y - entity1->chunk_y) +
(entity1->chunk_z - entity2->chunk_z);
break;
case ENTITYSTAT_COLUM_UUID:
diff = memcmp(&entity1->uuid_most_sig, &entity2->uuid_most_sig, 16);
break;
case ENTITYSTAT_COLUM_TICKS:
diff = entity1->ticks - entity2->ticks;
break;
case ENTITYSTAT_COLUM_MAX_HEALTH:
if(entity1->is_living_entity && entity2->is_living_entity) {
if(entity1->living.max_health < entity2->living.max_health) diff = -1;
else if(entity1->living.max_health > entity2->living.max_health) diff = 1;
else diff = 0;
} else diff = entity1->is_living_entity - entity2->is_living_entity;
break;
case ENTITYSTAT_COLUM_HEALTH:
if(entity1->is_living_entity && entity2->is_living_entity) {
if(entity1->living.health < entity2->living.health) diff = -1;
else if(entity1->living.health > entity2->living.health) diff = 1;
else diff = 0;
} else if(entity1->is_item && entity2->is_item) {
/*
if(entity1->item.health < entity2->item.health) diff = -1;
else if(entity1->item.health > entity2->item.health) diff = 1;
else diff = 0;
*/
diff = entity1->item.health - entity2->item.health;
} else {
diff = (entity1->is_living_entity - entity2->is_living_entity) +
(entity1->is_item - entity2->is_item);
}
break;
case ENTITYSTAT_COLUM_PERCENTAGE_HEALTH:
if(entity1->is_living_entity && entity2->is_living_entity) {
double p1 = (double)entity1->living.health / entity1->living.max_health;
double p2 = (double)entity1->living.health / entity1->living.max_health;
if(p1 < p2) diff = -1;
else if(p1 > p2) diff = 1;
else diff = 0;
} else diff = entity1->is_living_entity - entity2->is_living_entity;
break;
case ENTITYSTAT_COLUM_IDLE_TIME:
diff = entity1->is_living_entity && entity2->is_living_entity ?
entity1->living.idle_time - entity2->living.idle_time :
entity1->is_living_entity - entity2->is_living_entity;
break;
case ENTITYSTAT_COLUM_ATTACK_TARGET_ID:
diff = entity1->is_living_entity && entity2->is_living_entity ?
entity1->living.attack_target_id - entity2->living.attack_target_id :
entity1->is_living_entity - entity2->is_living_entity;
break;
case ENTITYSTAT_COLUM_GROWING_AGE:
diff = entity1->is_living_entity && entity2->is_living_entity ?
entity1->living.growing_age - entity2->living.growing_age :
entity1->is_living_entity - entity2->is_living_entity;
break;
case ENTITYSTAT_COLUM_ITEM_AGE:
diff = entity1->is_item && entity2->is_item ?
entity1->item.age - entity2->item.age :
entity1->is_item - entity2->is_item;
break;
case ENTITYSTAT_COLUM_ITEM_PICKUP_DELAY:
diff = entity1->is_item && entity2->is_item ?
entity1->item.pickup_delay - entity2->item.pickup_delay :
entity1->is_item - entity2->is_item;
break;
case ENTITYSTAT_COLUM_OWNER:
diff = entity1->is_item && entity2->is_item ?
(entity1->item.owner_offset >= 0 && entity1->item.owner_offset >= 0 ?
strcmp((const char *)entity1->data + entity1->item.owner_offset,
(const char *)entity2->data + entity2->item.owner_offset) :
entity1->item.owner_offset - entity2->item.owner_offset) :
entity1->is_item - entity2->is_item;
break;
case ENTITYSTAT_COLUM_THROWER:
diff = entity1->is_item && entity2->is_item ?
(entity1->item.thrower_offset >= 0 && entity2->item.thrower_offset >= 0 ?
strcmp((const char *)entity1->data + entity1->item.thrower_offset,
(const char *)entity2->data + entity2->item.thrower_offset) :
entity1->item.thrower_offset - entity2->item.thrower_offset) :
entity1->is_item - entity2->is_item;
break;
case ENTITYSTAT_COLUM_ITEM_DAMAGE:
diff = entity1->is_item && entity2->is_item ?
entity1->item.item_damage - entity2->item.item_damage :
entity1->is_item - entity2->is_item;
break;
case ENTITYSTAT_COLUM_ITEM_LIFESPAN:
diff = entity1->is_item && entity2->is_item ?
entity1->item.lifespan - entity2->item.lifespan :
entity1->is_item - entity2->is_item;
break;
case ENTITYSTAT_COLUM_ITEM_COUNT:
diff = entity1->is_item && entity2->is_item ?
entity1->item.item_count - entity2->item.item_count :
entity1->is_item - entity2->is_item;
break;
default:
diff = 0;
break;
}
if(diff) return diff * sort_configuration[i].direction;
} while(++i < sort_key_count);
return 0;
}
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;
}
if(sort_key_count) qsort(entities, count, sizeof(struct entity *), compare_entities);
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_TICKS:
output[row_i][colum_i] = xasprintf("%u", (unsigned int)entity->ticks);
break;
case ENTITYSTAT_COLUM_MAX_HEALTH:
output[row_i][colum_i] = entity->is_living_entity ?
xasprintf("%g", (double)entity->living.max_health) : "-";
break;
case ENTITYSTAT_COLUM_HEALTH:
output[row_i][colum_i] = entity->is_living_entity ?
xasprintf("%g", (double)entity->living.health) :
(entity->is_item ? xasprintf("%d", (int)entity->item.health) : "-");
break;
case ENTITYSTAT_COLUM_PERCENTAGE_HEALTH:
output[row_i][colum_i] = entity->is_living_entity ?
xasprintf("%g%%", (double)entity->living.health / entity->living.max_health * 100) : "-";
break;
case ENTITYSTAT_COLUM_IDLE_TIME:
output[row_i][colum_i] = entity->is_living_entity ?
xasprintf("%u", (unsigned int)entity->living.idle_time) : "-";
break;
case ENTITYSTAT_COLUM_ATTACK_TARGET_ID:
output[row_i][colum_i] = entity->is_living_entity && entity->living.attack_target_id != (uint32_t)-1 ?
xasprintf("%u", (unsigned int)entity->living.attack_target_id) : "-";
break;
case ENTITYSTAT_COLUM_GROWING_AGE:
output[row_i][colum_i] = entity->is_living_entity ?
xasprintf("%d", (int)entity->living.growing_age) : "-";
break;
case ENTITYSTAT_COLUM_ITEM_AGE:
output[row_i][colum_i] = entity->is_item ?
xasprintf("%d", (int)entity->item.age) : "-";
break;
case ENTITYSTAT_COLUM_ITEM_PICKUP_DELAY:
output[row_i][colum_i] = entity->is_item ?
xasprintf("%d", (int)entity->item.pickup_delay) : "-";
break;
case ENTITYSTAT_COLUM_OWNER:
output[row_i][colum_i] = entity->is_item && entity->item.owner_offset >= 0 ?
(const char *)(entity->data + entity->item.owner_offset) : "-";
break;
case ENTITYSTAT_COLUM_THROWER:
output[row_i][colum_i] = entity->is_item && entity->item.thrower_offset >= 0 ?
(const char *)(entity->data + entity->item.thrower_offset) : "-";
break;
case ENTITYSTAT_COLUM_ITEM_DAMAGE:
output[row_i][colum_i] = entity->is_item ?
xasprintf("%d", (int)entity->item.item_damage) : "-";
break;
case ENTITYSTAT_COLUM_ITEM_LIFESPAN:
output[row_i][colum_i] = entity->is_item ?
xasprintf("%d", (int)entity->item.lifespan) : "-";
break;
case ENTITYSTAT_COLUM_ITEM_COUNT:
output[row_i][colum_i] = entity->is_item ?
xasprintf("%hhu", entity->item.item_count) : "-";
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');
}
}