1133 lines
35 KiB
C
1133 lines
35 KiB
C
/**
|
|
* @file addons/rest.c
|
|
* @brief Rest addon.
|
|
*/
|
|
|
|
#include "../private_api.h"
|
|
|
|
#ifdef FLECS_REST
|
|
|
|
static ECS_TAG_DECLARE(EcsRestPlecs);
|
|
|
|
typedef struct {
|
|
ecs_world_t *world;
|
|
ecs_http_server_t *srv;
|
|
int32_t rc;
|
|
} ecs_rest_ctx_t;
|
|
|
|
/* Global statistics */
|
|
int64_t ecs_rest_request_count = 0;
|
|
int64_t ecs_rest_entity_count = 0;
|
|
int64_t ecs_rest_entity_error_count = 0;
|
|
int64_t ecs_rest_query_count = 0;
|
|
int64_t ecs_rest_query_error_count = 0;
|
|
int64_t ecs_rest_query_name_count = 0;
|
|
int64_t ecs_rest_query_name_error_count = 0;
|
|
int64_t ecs_rest_query_name_from_cache_count = 0;
|
|
int64_t ecs_rest_enable_count = 0;
|
|
int64_t ecs_rest_enable_error_count = 0;
|
|
int64_t ecs_rest_delete_count = 0;
|
|
int64_t ecs_rest_delete_error_count = 0;
|
|
int64_t ecs_rest_world_stats_count = 0;
|
|
int64_t ecs_rest_pipeline_stats_count = 0;
|
|
int64_t ecs_rest_stats_error_count = 0;
|
|
|
|
static ECS_COPY(EcsRest, dst, src, {
|
|
ecs_rest_ctx_t *impl = src->impl;
|
|
if (impl) {
|
|
impl->rc ++;
|
|
}
|
|
|
|
ecs_os_strset(&dst->ipaddr, src->ipaddr);
|
|
dst->port = src->port;
|
|
dst->impl = impl;
|
|
})
|
|
|
|
static ECS_MOVE(EcsRest, dst, src, {
|
|
*dst = *src;
|
|
src->ipaddr = NULL;
|
|
src->impl = NULL;
|
|
})
|
|
|
|
static ECS_DTOR(EcsRest, ptr, {
|
|
ecs_rest_ctx_t *impl = ptr->impl;
|
|
if (impl) {
|
|
impl->rc --;
|
|
if (!impl->rc) {
|
|
ecs_http_server_fini(impl->srv);
|
|
ecs_os_free(impl);
|
|
}
|
|
}
|
|
ecs_os_free(ptr->ipaddr);
|
|
})
|
|
|
|
static char *rest_last_err;
|
|
static ecs_os_api_log_t rest_prev_log;
|
|
|
|
static
|
|
void flecs_rest_capture_log(
|
|
int32_t level,
|
|
const char *file,
|
|
int32_t line,
|
|
const char *msg)
|
|
{
|
|
(void)file; (void)line;
|
|
|
|
if (level < 0) {
|
|
if (rest_prev_log) {
|
|
// Also log to previous log function
|
|
ecs_log_enable_colors(true);
|
|
rest_prev_log(level, file, line, msg);
|
|
ecs_log_enable_colors(false);
|
|
}
|
|
}
|
|
|
|
if (!rest_last_err && level < 0) {
|
|
rest_last_err = ecs_os_strdup(msg);
|
|
}
|
|
}
|
|
|
|
static
|
|
char* flecs_rest_get_captured_log(void) {
|
|
char *result = rest_last_err;
|
|
rest_last_err = NULL;
|
|
return result;
|
|
}
|
|
|
|
static
|
|
void flecs_reply_verror(
|
|
ecs_http_reply_t *reply,
|
|
const char *fmt,
|
|
va_list args)
|
|
{
|
|
ecs_strbuf_appendlit(&reply->body, "{\"error\":\"");
|
|
ecs_strbuf_vappend(&reply->body, fmt, args);
|
|
ecs_strbuf_appendlit(&reply->body, "\"}");
|
|
}
|
|
|
|
static
|
|
void flecs_reply_error(
|
|
ecs_http_reply_t *reply,
|
|
const char *fmt,
|
|
...)
|
|
{
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
flecs_reply_verror(reply, fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
static
|
|
void flecs_rest_bool_param(
|
|
const ecs_http_request_t *req,
|
|
const char *name,
|
|
bool *value_out)
|
|
{
|
|
const char *value = ecs_http_get_param(req, name);
|
|
if (value) {
|
|
if (!ecs_os_strcmp(value, "true")) {
|
|
value_out[0] = true;
|
|
} else {
|
|
value_out[0] = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
static
|
|
void flecs_rest_int_param(
|
|
const ecs_http_request_t *req,
|
|
const char *name,
|
|
int32_t *value_out)
|
|
{
|
|
const char *value = ecs_http_get_param(req, name);
|
|
if (value) {
|
|
*value_out = atoi(value);
|
|
}
|
|
}
|
|
|
|
static
|
|
void flecs_rest_string_param(
|
|
const ecs_http_request_t *req,
|
|
const char *name,
|
|
char **value_out)
|
|
{
|
|
const char *value = ecs_http_get_param(req, name);
|
|
if (value) {
|
|
*value_out = ECS_CONST_CAST(char*, value);
|
|
}
|
|
}
|
|
|
|
static
|
|
void flecs_rest_parse_json_ser_entity_params(
|
|
ecs_world_t *world,
|
|
ecs_entity_to_json_desc_t *desc,
|
|
const ecs_http_request_t *req)
|
|
{
|
|
flecs_rest_bool_param(req, "path", &desc->serialize_path);
|
|
flecs_rest_bool_param(req, "label", &desc->serialize_label);
|
|
flecs_rest_bool_param(req, "brief", &desc->serialize_brief);
|
|
flecs_rest_bool_param(req, "link", &desc->serialize_link);
|
|
flecs_rest_bool_param(req, "color", &desc->serialize_color);
|
|
flecs_rest_bool_param(req, "ids", &desc->serialize_ids);
|
|
flecs_rest_bool_param(req, "id_labels", &desc->serialize_id_labels);
|
|
flecs_rest_bool_param(req, "base", &desc->serialize_base);
|
|
flecs_rest_bool_param(req, "values", &desc->serialize_values);
|
|
flecs_rest_bool_param(req, "private", &desc->serialize_private);
|
|
flecs_rest_bool_param(req, "type_info", &desc->serialize_type_info);
|
|
flecs_rest_bool_param(req, "matches", &desc->serialize_matches);
|
|
flecs_rest_bool_param(req, "alerts", &desc->serialize_alerts);
|
|
|
|
char *rel = NULL;
|
|
flecs_rest_string_param(req, "refs", &rel);
|
|
if (rel) {
|
|
desc->serialize_refs = ecs_lookup_fullpath(world, rel);
|
|
}
|
|
}
|
|
|
|
static
|
|
void flecs_rest_parse_json_ser_iter_params(
|
|
ecs_iter_to_json_desc_t *desc,
|
|
const ecs_http_request_t *req)
|
|
{
|
|
flecs_rest_bool_param(req, "term_ids", &desc->serialize_term_ids);
|
|
flecs_rest_bool_param(req, "term_labels", &desc->serialize_term_labels);
|
|
flecs_rest_bool_param(req, "ids", &desc->serialize_ids);
|
|
flecs_rest_bool_param(req, "id_labels", &desc->serialize_id_labels);
|
|
flecs_rest_bool_param(req, "sources", &desc->serialize_sources);
|
|
flecs_rest_bool_param(req, "variables", &desc->serialize_variables);
|
|
flecs_rest_bool_param(req, "is_set", &desc->serialize_is_set);
|
|
flecs_rest_bool_param(req, "values", &desc->serialize_values);
|
|
flecs_rest_bool_param(req, "private", &desc->serialize_private);
|
|
flecs_rest_bool_param(req, "entities", &desc->serialize_entities);
|
|
flecs_rest_bool_param(req, "entity_labels", &desc->serialize_entity_labels);
|
|
flecs_rest_bool_param(req, "variable_labels", &desc->serialize_variable_labels);
|
|
flecs_rest_bool_param(req, "variable_ids", &desc->serialize_variable_ids);
|
|
flecs_rest_bool_param(req, "colors", &desc->serialize_colors);
|
|
flecs_rest_bool_param(req, "duration", &desc->measure_eval_duration);
|
|
flecs_rest_bool_param(req, "type_info", &desc->serialize_type_info);
|
|
flecs_rest_bool_param(req, "table", &desc->serialize_table);
|
|
}
|
|
|
|
static
|
|
bool flecs_rest_reply_entity(
|
|
ecs_world_t *world,
|
|
const ecs_http_request_t* req,
|
|
ecs_http_reply_t *reply)
|
|
{
|
|
char *path = &req->path[7];
|
|
ecs_dbg_2("rest: request entity '%s'", path);
|
|
|
|
ecs_os_linc(&ecs_rest_entity_count);
|
|
|
|
ecs_entity_t e = ecs_lookup_path_w_sep(
|
|
world, 0, path, "/", NULL, false);
|
|
if (!e) {
|
|
ecs_dbg_2("rest: entity '%s' not found", path);
|
|
flecs_reply_error(reply, "entity '%s' not found", path);
|
|
reply->code = 404;
|
|
ecs_os_linc(&ecs_rest_entity_error_count);
|
|
return true;
|
|
}
|
|
|
|
ecs_entity_to_json_desc_t desc = ECS_ENTITY_TO_JSON_INIT;
|
|
flecs_rest_parse_json_ser_entity_params(world, &desc, req);
|
|
if (ecs_entity_to_json_buf(world, e, &reply->body, &desc) != 0) {
|
|
ecs_strbuf_reset(&reply->body);
|
|
reply->code = 500;
|
|
reply->status = "Internal server error";
|
|
return true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static
|
|
bool flecs_rest_reply_world(
|
|
ecs_world_t *world,
|
|
const ecs_http_request_t* req,
|
|
ecs_http_reply_t *reply)
|
|
{
|
|
(void)req;
|
|
if (ecs_world_to_json_buf(world, &reply->body, NULL) != 0) {
|
|
ecs_strbuf_reset(&reply->body);
|
|
reply->code = 500;
|
|
reply->status = "Internal server error";
|
|
return true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static
|
|
ecs_entity_t flecs_rest_entity_from_path(
|
|
ecs_world_t *world,
|
|
ecs_http_reply_t *reply,
|
|
const char *path)
|
|
{
|
|
ecs_entity_t e = ecs_lookup_path_w_sep(
|
|
world, 0, path, "/", NULL, false);
|
|
if (!e) {
|
|
flecs_reply_error(reply, "entity '%s' not found", path);
|
|
reply->code = 404;
|
|
}
|
|
return e;
|
|
}
|
|
|
|
static
|
|
bool flecs_rest_set(
|
|
ecs_world_t *world,
|
|
const ecs_http_request_t* req,
|
|
ecs_http_reply_t *reply,
|
|
const char *path)
|
|
{
|
|
ecs_entity_t e;
|
|
if (!(e = flecs_rest_entity_from_path(world, reply, path))) {
|
|
return true;
|
|
}
|
|
|
|
const char *data = ecs_http_get_param(req, "data");
|
|
ecs_from_json_desc_t desc = {0};
|
|
desc.expr = data;
|
|
desc.name = path;
|
|
if (ecs_entity_from_json(world, e, data, &desc) == NULL) {
|
|
flecs_reply_error(reply, "invalid request");
|
|
reply->code = 400;
|
|
return true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static
|
|
bool flecs_rest_delete(
|
|
ecs_world_t *world,
|
|
ecs_http_reply_t *reply,
|
|
const char *path)
|
|
{
|
|
ecs_entity_t e;
|
|
if (!(e = flecs_rest_entity_from_path(world, reply, path))) {
|
|
ecs_os_linc(&ecs_rest_delete_error_count);
|
|
return true;
|
|
}
|
|
|
|
ecs_delete(world, e);
|
|
|
|
return true;
|
|
}
|
|
|
|
static
|
|
bool flecs_rest_enable(
|
|
ecs_world_t *world,
|
|
ecs_http_reply_t *reply,
|
|
const char *path,
|
|
bool enable)
|
|
{
|
|
ecs_os_linc(&ecs_rest_enable_count);
|
|
|
|
ecs_entity_t e;
|
|
if (!(e = flecs_rest_entity_from_path(world, reply, path))) {
|
|
ecs_os_linc(&ecs_rest_enable_error_count);
|
|
return true;
|
|
}
|
|
|
|
ecs_enable(world, e, enable);
|
|
|
|
return true;
|
|
}
|
|
|
|
static
|
|
bool flecs_rest_script(
|
|
ecs_world_t *world,
|
|
const ecs_http_request_t* req,
|
|
ecs_http_reply_t *reply)
|
|
{
|
|
(void)world;
|
|
(void)req;
|
|
(void)reply;
|
|
#ifdef FLECS_PLECS
|
|
const char *data = ecs_http_get_param(req, "data");
|
|
if (!data) {
|
|
flecs_reply_error(reply, "missing data parameter");
|
|
return true;
|
|
}
|
|
|
|
bool prev_color = ecs_log_enable_colors(false);
|
|
rest_prev_log = ecs_os_api.log_;
|
|
ecs_os_api.log_ = flecs_rest_capture_log;
|
|
|
|
ecs_entity_t script = ecs_script(world, {
|
|
.entity = ecs_entity(world, { .name = "scripts.main" }),
|
|
.str = data
|
|
});
|
|
|
|
if (!script) {
|
|
char *err = flecs_rest_get_captured_log();
|
|
char *escaped_err = ecs_astresc('"', err);
|
|
flecs_reply_error(reply, escaped_err);
|
|
ecs_os_linc(&ecs_rest_query_error_count);
|
|
reply->code = 400; /* bad request */
|
|
ecs_os_free(escaped_err);
|
|
ecs_os_free(err);
|
|
}
|
|
|
|
ecs_os_api.log_ = rest_prev_log;
|
|
ecs_log_enable_colors(prev_color);
|
|
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
static
|
|
void flecs_rest_reply_set_captured_log(
|
|
ecs_http_reply_t *reply)
|
|
{
|
|
char *err = flecs_rest_get_captured_log();
|
|
if (err) {
|
|
char *escaped_err = ecs_astresc('"', err);
|
|
flecs_reply_error(reply, escaped_err);
|
|
reply->code = 400;
|
|
ecs_os_free(escaped_err);
|
|
ecs_os_free(err);
|
|
}
|
|
}
|
|
|
|
static
|
|
int flecs_rest_iter_to_reply(
|
|
ecs_world_t *world,
|
|
const ecs_http_request_t* req,
|
|
ecs_http_reply_t *reply,
|
|
ecs_iter_t *it)
|
|
{
|
|
ecs_iter_to_json_desc_t desc = {0};
|
|
desc.serialize_entities = true;
|
|
desc.serialize_variables = true;
|
|
flecs_rest_parse_json_ser_iter_params(&desc, req);
|
|
|
|
int32_t offset = 0;
|
|
int32_t limit = 1000;
|
|
|
|
flecs_rest_int_param(req, "offset", &offset);
|
|
flecs_rest_int_param(req, "limit", &limit);
|
|
|
|
if (offset < 0 || limit < 0) {
|
|
flecs_reply_error(reply, "invalid offset/limit parameter");
|
|
reply->code = 400;
|
|
return -1;
|
|
}
|
|
|
|
ecs_iter_t pit = ecs_page_iter(it, offset, limit);
|
|
if (ecs_iter_to_json_buf(world, &pit, &reply->body, &desc)) {
|
|
flecs_rest_reply_set_captured_log(reply);
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static
|
|
bool flecs_rest_reply_existing_query(
|
|
ecs_world_t *world,
|
|
const ecs_http_request_t* req,
|
|
ecs_http_reply_t *reply,
|
|
const char *name)
|
|
{
|
|
ecs_os_linc(&ecs_rest_query_name_count);
|
|
|
|
ecs_entity_t q = ecs_lookup_fullpath(world, name);
|
|
if (!q) {
|
|
flecs_reply_error(reply, "unresolved identifier '%s'", name);
|
|
reply->code = 404;
|
|
ecs_os_linc(&ecs_rest_query_name_error_count);
|
|
return true;
|
|
}
|
|
|
|
const EcsPoly *poly = ecs_get_pair(world, q, EcsPoly, EcsQuery);
|
|
if (!poly) {
|
|
flecs_reply_error(reply,
|
|
"resolved identifier '%s' is not a query", name);
|
|
reply->code = 400;
|
|
ecs_os_linc(&ecs_rest_query_name_error_count);
|
|
return true;
|
|
}
|
|
|
|
ecs_iter_t it;
|
|
ecs_iter_poly(world, poly->poly, &it, NULL);
|
|
|
|
ecs_dbg_2("rest: request query '%s'", q);
|
|
bool prev_color = ecs_log_enable_colors(false);
|
|
rest_prev_log = ecs_os_api.log_;
|
|
ecs_os_api.log_ = flecs_rest_capture_log;
|
|
|
|
const char *vars = ecs_http_get_param(req, "vars");
|
|
if (vars) {
|
|
if (!ecs_poly_is(poly->poly, ecs_rule_t)) {
|
|
flecs_reply_error(reply,
|
|
"variables are only supported for rule queries");
|
|
reply->code = 400;
|
|
ecs_os_linc(&ecs_rest_query_name_error_count);
|
|
return true;
|
|
}
|
|
if (ecs_rule_parse_vars(poly->poly, &it, vars) == NULL) {
|
|
flecs_rest_reply_set_captured_log(reply);
|
|
ecs_os_linc(&ecs_rest_query_name_error_count);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
flecs_rest_iter_to_reply(world, req, reply, &it);
|
|
|
|
ecs_os_api.log_ = rest_prev_log;
|
|
ecs_log_enable_colors(prev_color);
|
|
|
|
return true;
|
|
}
|
|
|
|
static
|
|
bool flecs_rest_reply_query(
|
|
ecs_world_t *world,
|
|
const ecs_http_request_t* req,
|
|
ecs_http_reply_t *reply)
|
|
{
|
|
const char *q_name = ecs_http_get_param(req, "name");
|
|
if (q_name) {
|
|
return flecs_rest_reply_existing_query(world, req, reply, q_name);
|
|
}
|
|
|
|
ecs_os_linc(&ecs_rest_query_count);
|
|
|
|
const char *q = ecs_http_get_param(req, "q");
|
|
if (!q) {
|
|
ecs_strbuf_appendlit(&reply->body, "Missing parameter 'q'");
|
|
reply->code = 400; /* bad request */
|
|
ecs_os_linc(&ecs_rest_query_error_count);
|
|
return true;
|
|
}
|
|
|
|
ecs_dbg_2("rest: request query '%s'", q);
|
|
bool prev_color = ecs_log_enable_colors(false);
|
|
rest_prev_log = ecs_os_api.log_;
|
|
ecs_os_api.log_ = flecs_rest_capture_log;
|
|
|
|
ecs_rule_t *r = ecs_rule_init(world, &(ecs_filter_desc_t){
|
|
.expr = q
|
|
});
|
|
if (!r) {
|
|
flecs_rest_reply_set_captured_log(reply);
|
|
ecs_os_linc(&ecs_rest_query_error_count);
|
|
} else {
|
|
ecs_iter_t it = ecs_rule_iter(world, r);
|
|
flecs_rest_iter_to_reply(world, req, reply, &it);
|
|
ecs_rule_fini(r);
|
|
}
|
|
|
|
ecs_os_api.log_ = rest_prev_log;
|
|
ecs_log_enable_colors(prev_color);
|
|
|
|
return true;
|
|
}
|
|
|
|
#ifdef FLECS_MONITOR
|
|
|
|
static
|
|
void flecs_rest_array_append_(
|
|
ecs_strbuf_t *reply,
|
|
const char *field,
|
|
int32_t field_len,
|
|
const ecs_float_t *values,
|
|
int32_t t)
|
|
{
|
|
ecs_strbuf_list_appendch(reply, '"');
|
|
ecs_strbuf_appendstrn(reply, field, field_len);
|
|
ecs_strbuf_appendlit(reply, "\":");
|
|
ecs_strbuf_list_push(reply, "[", ",");
|
|
|
|
int32_t i;
|
|
for (i = t + 1; i <= (t + ECS_STAT_WINDOW); i ++) {
|
|
int32_t index = i % ECS_STAT_WINDOW;
|
|
ecs_strbuf_list_next(reply);
|
|
ecs_strbuf_appendflt(reply, (double)values[index], '"');
|
|
}
|
|
|
|
ecs_strbuf_list_pop(reply, "]");
|
|
}
|
|
|
|
#define flecs_rest_array_append(reply, field, values, t)\
|
|
flecs_rest_array_append_(reply, field, sizeof(field) - 1, values, t)
|
|
|
|
static
|
|
void flecs_rest_gauge_append(
|
|
ecs_strbuf_t *reply,
|
|
const ecs_metric_t *m,
|
|
const char *field,
|
|
int32_t field_len,
|
|
int32_t t,
|
|
const char *brief,
|
|
int32_t brief_len)
|
|
{
|
|
ecs_strbuf_list_appendch(reply, '"');
|
|
ecs_strbuf_appendstrn(reply, field, field_len);
|
|
ecs_strbuf_appendlit(reply, "\":");
|
|
ecs_strbuf_list_push(reply, "{", ",");
|
|
|
|
flecs_rest_array_append(reply, "avg", m->gauge.avg, t);
|
|
flecs_rest_array_append(reply, "min", m->gauge.min, t);
|
|
flecs_rest_array_append(reply, "max", m->gauge.max, t);
|
|
|
|
if (brief) {
|
|
ecs_strbuf_list_appendlit(reply, "\"brief\":\"");
|
|
ecs_strbuf_appendstrn(reply, brief, brief_len);
|
|
ecs_strbuf_appendch(reply, '"');
|
|
}
|
|
|
|
ecs_strbuf_list_pop(reply, "}");
|
|
}
|
|
|
|
static
|
|
void flecs_rest_counter_append(
|
|
ecs_strbuf_t *reply,
|
|
const ecs_metric_t *m,
|
|
const char *field,
|
|
int32_t field_len,
|
|
int32_t t,
|
|
const char *brief,
|
|
int32_t brief_len)
|
|
{
|
|
flecs_rest_gauge_append(reply, m, field, field_len, t, brief, brief_len);
|
|
}
|
|
|
|
#define ECS_GAUGE_APPEND_T(reply, s, field, t, brief)\
|
|
flecs_rest_gauge_append(reply, &(s)->field, #field, sizeof(#field) - 1, t, brief, sizeof(brief) - 1)
|
|
|
|
#define ECS_COUNTER_APPEND_T(reply, s, field, t, brief)\
|
|
flecs_rest_counter_append(reply, &(s)->field, #field, sizeof(#field) - 1, t, brief, sizeof(brief) - 1)
|
|
|
|
#define ECS_GAUGE_APPEND(reply, s, field, brief)\
|
|
ECS_GAUGE_APPEND_T(reply, s, field, (s)->t, brief)
|
|
|
|
#define ECS_COUNTER_APPEND(reply, s, field, brief)\
|
|
ECS_COUNTER_APPEND_T(reply, s, field, (s)->t, brief)
|
|
|
|
static
|
|
void flecs_world_stats_to_json(
|
|
ecs_strbuf_t *reply,
|
|
const EcsWorldStats *monitor_stats)
|
|
{
|
|
const ecs_world_stats_t *stats = &monitor_stats->stats;
|
|
|
|
ecs_strbuf_list_push(reply, "{", ",");
|
|
ECS_GAUGE_APPEND(reply, stats, entities.count, "Alive entity ids in the world");
|
|
ECS_GAUGE_APPEND(reply, stats, entities.not_alive_count, "Not alive entity ids in the world");
|
|
|
|
ECS_GAUGE_APPEND(reply, stats, performance.fps, "Frames per second");
|
|
ECS_COUNTER_APPEND(reply, stats, performance.frame_time, "Time spent in frame");
|
|
ECS_COUNTER_APPEND(reply, stats, performance.system_time, "Time spent on running systems in frame");
|
|
ECS_COUNTER_APPEND(reply, stats, performance.emit_time, "Time spent on notifying observers in frame");
|
|
ECS_COUNTER_APPEND(reply, stats, performance.merge_time, "Time spent on merging commands in frame");
|
|
ECS_COUNTER_APPEND(reply, stats, performance.rematch_time, "Time spent on revalidating query caches in frame");
|
|
|
|
ECS_COUNTER_APPEND(reply, stats, commands.add_count, "Add commands executed");
|
|
ECS_COUNTER_APPEND(reply, stats, commands.remove_count, "Remove commands executed");
|
|
ECS_COUNTER_APPEND(reply, stats, commands.delete_count, "Delete commands executed");
|
|
ECS_COUNTER_APPEND(reply, stats, commands.clear_count, "Clear commands executed");
|
|
ECS_COUNTER_APPEND(reply, stats, commands.set_count, "Set commands executed");
|
|
ECS_COUNTER_APPEND(reply, stats, commands.get_mut_count, "Get_mut commands executed");
|
|
ECS_COUNTER_APPEND(reply, stats, commands.modified_count, "Modified commands executed");
|
|
ECS_COUNTER_APPEND(reply, stats, commands.other_count, "Misc commands executed");
|
|
ECS_COUNTER_APPEND(reply, stats, commands.discard_count, "Commands for already deleted entities");
|
|
ECS_COUNTER_APPEND(reply, stats, commands.batched_entity_count, "Entities with batched commands");
|
|
ECS_COUNTER_APPEND(reply, stats, commands.batched_count, "Number of commands batched");
|
|
|
|
ECS_COUNTER_APPEND(reply, stats, frame.merge_count, "Number of merges (sync points)");
|
|
ECS_COUNTER_APPEND(reply, stats, frame.pipeline_build_count, "Pipeline rebuilds (happen when systems become active/enabled)");
|
|
ECS_COUNTER_APPEND(reply, stats, frame.systems_ran, "Systems ran in frame");
|
|
ECS_COUNTER_APPEND(reply, stats, frame.observers_ran, "Number of times an observer was invoked in frame");
|
|
ECS_COUNTER_APPEND(reply, stats, frame.event_emit_count, "Events emitted in frame");
|
|
ECS_COUNTER_APPEND(reply, stats, frame.rematch_count, "Number of query cache revalidations");
|
|
|
|
ECS_GAUGE_APPEND(reply, stats, tables.count, "Tables in the world (including empty)");
|
|
ECS_GAUGE_APPEND(reply, stats, tables.empty_count, "Empty tables in the world");
|
|
ECS_GAUGE_APPEND(reply, stats, tables.tag_only_count, "Tables with only tags");
|
|
ECS_GAUGE_APPEND(reply, stats, tables.trivial_only_count, "Tables with only trivial types (no hooks)");
|
|
ECS_GAUGE_APPEND(reply, stats, tables.record_count, "Table records registered with search indices");
|
|
ECS_GAUGE_APPEND(reply, stats, tables.storage_count, "Component storages for all tables");
|
|
ECS_COUNTER_APPEND(reply, stats, tables.create_count, "Number of new tables created");
|
|
ECS_COUNTER_APPEND(reply, stats, tables.delete_count, "Number of tables deleted");
|
|
|
|
ECS_GAUGE_APPEND(reply, stats, ids.count, "Component, tag and pair ids in use");
|
|
ECS_GAUGE_APPEND(reply, stats, ids.tag_count, "Tag ids in use");
|
|
ECS_GAUGE_APPEND(reply, stats, ids.component_count, "Component ids in use");
|
|
ECS_GAUGE_APPEND(reply, stats, ids.pair_count, "Pair ids in use");
|
|
ECS_GAUGE_APPEND(reply, stats, ids.wildcard_count, "Wildcard ids in use");
|
|
ECS_GAUGE_APPEND(reply, stats, ids.type_count, "Registered component types");
|
|
ECS_COUNTER_APPEND(reply, stats, ids.create_count, "Number of new component, tag and pair ids created");
|
|
ECS_COUNTER_APPEND(reply, stats, ids.delete_count, "Number of component, pair and tag ids deleted");
|
|
|
|
ECS_GAUGE_APPEND(reply, stats, queries.query_count, "Queries in the world");
|
|
ECS_GAUGE_APPEND(reply, stats, queries.observer_count, "Observers in the world");
|
|
ECS_GAUGE_APPEND(reply, stats, queries.system_count, "Systems in the world");
|
|
|
|
ECS_COUNTER_APPEND(reply, stats, memory.alloc_count, "Allocations by OS API");
|
|
ECS_COUNTER_APPEND(reply, stats, memory.realloc_count, "Reallocs by OS API");
|
|
ECS_COUNTER_APPEND(reply, stats, memory.free_count, "Frees by OS API");
|
|
ECS_GAUGE_APPEND(reply, stats, memory.outstanding_alloc_count, "Outstanding allocations by OS API");
|
|
ECS_COUNTER_APPEND(reply, stats, memory.block_alloc_count, "Blocks allocated by block allocators");
|
|
ECS_COUNTER_APPEND(reply, stats, memory.block_free_count, "Blocks freed by block allocators");
|
|
ECS_GAUGE_APPEND(reply, stats, memory.block_outstanding_alloc_count, "Outstanding block allocations");
|
|
ECS_COUNTER_APPEND(reply, stats, memory.stack_alloc_count, "Pages allocated by stack allocators");
|
|
ECS_COUNTER_APPEND(reply, stats, memory.stack_free_count, "Pages freed by stack allocators");
|
|
ECS_GAUGE_APPEND(reply, stats, memory.stack_outstanding_alloc_count, "Outstanding page allocations");
|
|
|
|
ECS_COUNTER_APPEND(reply, stats, rest.request_count, "Received requests");
|
|
ECS_COUNTER_APPEND(reply, stats, rest.entity_count, "Received entity/ requests");
|
|
ECS_COUNTER_APPEND(reply, stats, rest.entity_error_count, "Failed entity/ requests");
|
|
ECS_COUNTER_APPEND(reply, stats, rest.query_count, "Received query/ requests");
|
|
ECS_COUNTER_APPEND(reply, stats, rest.query_error_count, "Failed query/ requests");
|
|
ECS_COUNTER_APPEND(reply, stats, rest.query_name_count, "Received named query/ requests");
|
|
ECS_COUNTER_APPEND(reply, stats, rest.query_name_error_count, "Failed named query/ requests");
|
|
ECS_COUNTER_APPEND(reply, stats, rest.query_name_from_cache_count, "Named query/ requests from cache");
|
|
ECS_COUNTER_APPEND(reply, stats, rest.enable_count, "Received enable/ and disable/ requests");
|
|
ECS_COUNTER_APPEND(reply, stats, rest.enable_error_count, "Failed enable/ and disable/ requests");
|
|
ECS_COUNTER_APPEND(reply, stats, rest.world_stats_count, "Received world stats requests");
|
|
ECS_COUNTER_APPEND(reply, stats, rest.pipeline_stats_count, "Received pipeline stats requests");
|
|
ECS_COUNTER_APPEND(reply, stats, rest.stats_error_count, "Failed stats requests");
|
|
|
|
ECS_COUNTER_APPEND(reply, stats, http.request_received_count, "Received requests");
|
|
ECS_COUNTER_APPEND(reply, stats, http.request_invalid_count, "Received invalid requests");
|
|
ECS_COUNTER_APPEND(reply, stats, http.request_handled_ok_count, "Requests handled successfully");
|
|
ECS_COUNTER_APPEND(reply, stats, http.request_handled_error_count, "Requests handled with error code");
|
|
ECS_COUNTER_APPEND(reply, stats, http.request_not_handled_count, "Requests not handled (unknown endpoint)");
|
|
ECS_COUNTER_APPEND(reply, stats, http.request_preflight_count, "Preflight requests received");
|
|
ECS_COUNTER_APPEND(reply, stats, http.send_ok_count, "Successful replies");
|
|
ECS_COUNTER_APPEND(reply, stats, http.send_error_count, "Unsuccessful replies");
|
|
ECS_COUNTER_APPEND(reply, stats, http.busy_count, "Dropped requests due to full send queue (503)");
|
|
|
|
ecs_strbuf_list_pop(reply, "}");
|
|
}
|
|
|
|
static
|
|
void flecs_system_stats_to_json(
|
|
ecs_world_t *world,
|
|
ecs_strbuf_t *reply,
|
|
ecs_entity_t system,
|
|
const ecs_system_stats_t *stats)
|
|
{
|
|
ecs_strbuf_list_push(reply, "{", ",");
|
|
ecs_strbuf_list_appendlit(reply, "\"name\":\"");
|
|
ecs_get_path_w_sep_buf(world, 0, system, ".", NULL, reply);
|
|
ecs_strbuf_appendch(reply, '"');
|
|
|
|
if (!stats->task) {
|
|
ECS_GAUGE_APPEND(reply, &stats->query, matched_table_count, "");
|
|
ECS_GAUGE_APPEND(reply, &stats->query, matched_entity_count, "");
|
|
}
|
|
|
|
ECS_COUNTER_APPEND_T(reply, stats, time_spent, stats->query.t, "");
|
|
ecs_strbuf_list_pop(reply, "}");
|
|
}
|
|
|
|
static
|
|
void flecs_pipeline_stats_to_json(
|
|
ecs_world_t *world,
|
|
ecs_strbuf_t *reply,
|
|
const EcsPipelineStats *stats)
|
|
{
|
|
ecs_strbuf_list_push(reply, "[", ",");
|
|
|
|
int32_t i, count = ecs_vec_count(&stats->stats.systems), sync_cur = 0;
|
|
ecs_entity_t *ids = ecs_vec_first_t(&stats->stats.systems, ecs_entity_t);
|
|
for (i = 0; i < count; i ++) {
|
|
ecs_entity_t id = ids[i];
|
|
|
|
ecs_strbuf_list_next(reply);
|
|
|
|
if (id) {
|
|
ecs_system_stats_t *sys_stats = ecs_map_get_deref(
|
|
&stats->stats.system_stats, ecs_system_stats_t, id);
|
|
flecs_system_stats_to_json(world, reply, id, sys_stats);
|
|
} else {
|
|
/* Sync point */
|
|
ecs_strbuf_list_push(reply, "{", ",");
|
|
ecs_sync_stats_t *sync_stats = ecs_vec_get_t(
|
|
&stats->stats.sync_points, ecs_sync_stats_t, sync_cur);
|
|
|
|
ecs_strbuf_list_appendlit(reply, "\"system_count\":");
|
|
ecs_strbuf_appendint(reply, sync_stats->system_count);
|
|
|
|
ecs_strbuf_list_appendlit(reply, "\"multi_threaded\":");
|
|
ecs_strbuf_appendbool(reply, sync_stats->multi_threaded);
|
|
|
|
ecs_strbuf_list_appendlit(reply, "\"no_readonly\":");
|
|
ecs_strbuf_appendbool(reply, sync_stats->no_readonly);
|
|
|
|
ECS_GAUGE_APPEND_T(reply, sync_stats,
|
|
time_spent, stats->stats.t, "");
|
|
ECS_GAUGE_APPEND_T(reply, sync_stats,
|
|
commands_enqueued, stats->stats.t, "");
|
|
|
|
ecs_strbuf_list_pop(reply, "}");
|
|
sync_cur ++;
|
|
}
|
|
}
|
|
|
|
ecs_strbuf_list_pop(reply, "]");
|
|
}
|
|
|
|
static
|
|
bool flecs_rest_reply_stats(
|
|
ecs_world_t *world,
|
|
const ecs_http_request_t* req,
|
|
ecs_http_reply_t *reply)
|
|
{
|
|
char *period_str = NULL;
|
|
flecs_rest_string_param(req, "period", &period_str);
|
|
char *category = &req->path[6];
|
|
|
|
ecs_entity_t period = EcsPeriod1s;
|
|
if (period_str) {
|
|
char *period_name = ecs_asprintf("Period%s", period_str);
|
|
period = ecs_lookup_child(world, ecs_id(FlecsMonitor), period_name);
|
|
ecs_os_free(period_name);
|
|
if (!period) {
|
|
flecs_reply_error(reply, "bad request (invalid period string)");
|
|
reply->code = 400;
|
|
ecs_os_linc(&ecs_rest_stats_error_count);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!ecs_os_strcmp(category, "world")) {
|
|
const EcsWorldStats *stats = ecs_get_pair(world, EcsWorld,
|
|
EcsWorldStats, period);
|
|
flecs_world_stats_to_json(&reply->body, stats);
|
|
ecs_os_linc(&ecs_rest_world_stats_count);
|
|
return true;
|
|
|
|
} else if (!ecs_os_strcmp(category, "pipeline")) {
|
|
const EcsPipelineStats *stats = ecs_get_pair(world, EcsWorld,
|
|
EcsPipelineStats, period);
|
|
flecs_pipeline_stats_to_json(world, &reply->body, stats);
|
|
ecs_os_linc(&ecs_rest_pipeline_stats_count);
|
|
return true;
|
|
|
|
} else {
|
|
flecs_reply_error(reply, "bad request (unsupported category)");
|
|
reply->code = 400;
|
|
ecs_os_linc(&ecs_rest_stats_error_count);
|
|
return false;
|
|
}
|
|
}
|
|
#else
|
|
static
|
|
bool flecs_rest_reply_stats(
|
|
ecs_world_t *world,
|
|
const ecs_http_request_t* req,
|
|
ecs_http_reply_t *reply)
|
|
{
|
|
(void)world;
|
|
(void)req;
|
|
(void)reply;
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
static
|
|
void flecs_rest_reply_table_append_type(
|
|
ecs_world_t *world,
|
|
ecs_strbuf_t *reply,
|
|
const ecs_table_t *table)
|
|
{
|
|
ecs_strbuf_list_push(reply, "[", ",");
|
|
int32_t i, count = table->type.count;
|
|
ecs_id_t *ids = table->type.array;
|
|
for (i = 0; i < count; i ++) {
|
|
ecs_strbuf_list_next(reply);
|
|
ecs_strbuf_appendch(reply, '"');
|
|
ecs_id_str_buf(world, ids[i], reply);
|
|
ecs_strbuf_appendch(reply, '"');
|
|
}
|
|
ecs_strbuf_list_pop(reply, "]");
|
|
}
|
|
|
|
static
|
|
void flecs_rest_reply_table_append_memory(
|
|
ecs_strbuf_t *reply,
|
|
const ecs_table_t *table)
|
|
{
|
|
int32_t used = 0, allocated = 0;
|
|
|
|
used += table->data.entities.count * ECS_SIZEOF(ecs_entity_t);
|
|
allocated += table->data.entities.size * ECS_SIZEOF(ecs_entity_t);
|
|
|
|
int32_t i, storage_count = table->column_count;
|
|
ecs_column_t *columns = table->data.columns;
|
|
|
|
for (i = 0; i < storage_count; i ++) {
|
|
used += columns[i].data.count * columns[i].ti->size;
|
|
allocated += columns[i].data.size * columns[i].ti->size;
|
|
}
|
|
|
|
ecs_strbuf_list_push(reply, "{", ",");
|
|
ecs_strbuf_list_append(reply, "\"used\":%d", used);
|
|
ecs_strbuf_list_append(reply, "\"allocated\":%d", allocated);
|
|
ecs_strbuf_list_pop(reply, "}");
|
|
}
|
|
|
|
static
|
|
void flecs_rest_reply_table_append(
|
|
ecs_world_t *world,
|
|
ecs_strbuf_t *reply,
|
|
const ecs_table_t *table)
|
|
{
|
|
ecs_strbuf_list_next(reply);
|
|
ecs_strbuf_list_push(reply, "{", ",");
|
|
ecs_strbuf_list_append(reply, "\"id\":%u", (uint32_t)table->id);
|
|
ecs_strbuf_list_appendstr(reply, "\"type\":");
|
|
flecs_rest_reply_table_append_type(world, reply, table);
|
|
ecs_strbuf_list_append(reply, "\"count\":%d", ecs_table_count(table));
|
|
ecs_strbuf_list_append(reply, "\"memory\":");
|
|
flecs_rest_reply_table_append_memory(reply, table);
|
|
ecs_strbuf_list_pop(reply, "}");
|
|
}
|
|
|
|
static
|
|
bool flecs_rest_reply_tables(
|
|
ecs_world_t *world,
|
|
const ecs_http_request_t* req,
|
|
ecs_http_reply_t *reply)
|
|
{
|
|
(void)req;
|
|
|
|
ecs_strbuf_list_push(&reply->body, "[", ",");
|
|
ecs_sparse_t *tables = &world->store.tables;
|
|
int32_t i, count = flecs_sparse_count(tables);
|
|
for (i = 0; i < count; i ++) {
|
|
ecs_table_t *table = flecs_sparse_get_dense_t(tables, ecs_table_t, i);
|
|
flecs_rest_reply_table_append(world, &reply->body, table);
|
|
}
|
|
ecs_strbuf_list_pop(&reply->body, "]");
|
|
|
|
return true;
|
|
}
|
|
|
|
static
|
|
bool flecs_rest_reply(
|
|
const ecs_http_request_t* req,
|
|
ecs_http_reply_t *reply,
|
|
void *ctx)
|
|
{
|
|
ecs_rest_ctx_t *impl = ctx;
|
|
ecs_world_t *world = impl->world;
|
|
|
|
ecs_os_linc(&ecs_rest_request_count);
|
|
|
|
if (req->path == NULL) {
|
|
ecs_dbg("rest: bad request (missing path)");
|
|
flecs_reply_error(reply, "bad request (missing path)");
|
|
reply->code = 400;
|
|
return false;
|
|
}
|
|
|
|
if (req->method == EcsHttpGet) {
|
|
/* Entity endpoint */
|
|
if (!ecs_os_strncmp(req->path, "entity/", 7)) {
|
|
return flecs_rest_reply_entity(world, req, reply);
|
|
|
|
/* Query endpoint */
|
|
} else if (!ecs_os_strcmp(req->path, "query")) {
|
|
return flecs_rest_reply_query(world, req, reply);
|
|
|
|
/* World endpoint */
|
|
} else if (!ecs_os_strcmp(req->path, "world")) {
|
|
return flecs_rest_reply_world(world, req, reply);
|
|
|
|
/* Stats endpoint */
|
|
} else if (!ecs_os_strncmp(req->path, "stats/", 6)) {
|
|
return flecs_rest_reply_stats(world, req, reply);
|
|
|
|
/* Tables endpoint */
|
|
} else if (!ecs_os_strncmp(req->path, "tables", 6)) {
|
|
return flecs_rest_reply_tables(world, req, reply);
|
|
}
|
|
|
|
} else if (req->method == EcsHttpPut) {
|
|
/* Set endpoint */
|
|
if (!ecs_os_strncmp(req->path, "set/", 4)) {
|
|
return flecs_rest_set(world, req, reply, &req->path[4]);
|
|
|
|
/* Delete endpoint */
|
|
} else if (!ecs_os_strncmp(req->path, "delete/", 7)) {
|
|
return flecs_rest_delete(world, reply, &req->path[7]);
|
|
|
|
/* Enable endpoint */
|
|
} else if (!ecs_os_strncmp(req->path, "enable/", 7)) {
|
|
return flecs_rest_enable(world, reply, &req->path[7], true);
|
|
|
|
/* Disable endpoint */
|
|
} else if (!ecs_os_strncmp(req->path, "disable/", 8)) {
|
|
return flecs_rest_enable(world, reply, &req->path[8], false);
|
|
|
|
/* Script endpoint */
|
|
} else if (!ecs_os_strncmp(req->path, "script", 6)) {
|
|
return flecs_rest_script(world, req, reply);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
ecs_http_server_t* ecs_rest_server_init(
|
|
ecs_world_t *world,
|
|
const ecs_http_server_desc_t *desc)
|
|
{
|
|
ecs_rest_ctx_t *srv_ctx = ecs_os_calloc_t(ecs_rest_ctx_t);
|
|
ecs_http_server_desc_t private_desc = {0};
|
|
if (desc) {
|
|
private_desc = *desc;
|
|
}
|
|
private_desc.callback = flecs_rest_reply;
|
|
private_desc.ctx = srv_ctx;
|
|
|
|
ecs_http_server_t *srv = ecs_http_server_init(&private_desc);
|
|
if (!srv) {
|
|
ecs_os_free(srv_ctx);
|
|
return NULL;
|
|
}
|
|
|
|
srv_ctx->world = world;
|
|
srv_ctx->srv = srv;
|
|
srv_ctx->rc = 1;
|
|
srv_ctx->srv = srv;
|
|
return srv;
|
|
}
|
|
|
|
void ecs_rest_server_fini(
|
|
ecs_http_server_t *srv)
|
|
{
|
|
ecs_rest_ctx_t *srv_ctx = ecs_http_server_ctx(srv);
|
|
ecs_os_free(srv_ctx);
|
|
ecs_http_server_fini(srv);
|
|
}
|
|
|
|
static
|
|
void flecs_on_set_rest(ecs_iter_t *it) {
|
|
EcsRest *rest = it->ptrs[0];
|
|
|
|
int i;
|
|
for(i = 0; i < it->count; i ++) {
|
|
if (!rest[i].port) {
|
|
rest[i].port = ECS_REST_DEFAULT_PORT;
|
|
}
|
|
|
|
ecs_http_server_t *srv = ecs_rest_server_init(it->real_world,
|
|
&(ecs_http_server_desc_t){
|
|
.ipaddr = rest[i].ipaddr,
|
|
.port = rest[i].port
|
|
});
|
|
|
|
if (!srv) {
|
|
const char *ipaddr = rest[i].ipaddr ? rest[i].ipaddr : "0.0.0.0";
|
|
ecs_err("failed to create REST server on %s:%u",
|
|
ipaddr, rest[i].port);
|
|
continue;
|
|
}
|
|
|
|
rest[i].impl = ecs_http_server_ctx(srv);
|
|
|
|
ecs_http_server_start(srv);
|
|
}
|
|
}
|
|
|
|
static
|
|
void DequeueRest(ecs_iter_t *it) {
|
|
EcsRest *rest = ecs_field(it, EcsRest, 1);
|
|
|
|
if (it->delta_system_time > (ecs_ftime_t)1.0) {
|
|
ecs_warn(
|
|
"detected large progress interval (%.2fs), REST request may timeout",
|
|
(double)it->delta_system_time);
|
|
}
|
|
|
|
int32_t i;
|
|
for(i = 0; i < it->count; i ++) {
|
|
ecs_rest_ctx_t *ctx = rest[i].impl;
|
|
if (ctx) {
|
|
ecs_http_server_dequeue(ctx->srv, it->delta_time);
|
|
}
|
|
}
|
|
}
|
|
|
|
static
|
|
void DisableRest(ecs_iter_t *it) {
|
|
ecs_world_t *world = it->world;
|
|
|
|
ecs_iter_t rit = ecs_term_iter(world, &(ecs_term_t){
|
|
.id = ecs_id(EcsRest),
|
|
.src.flags = EcsSelf
|
|
});
|
|
|
|
if (it->event == EcsOnAdd) {
|
|
/* REST module was disabled */
|
|
while (ecs_term_next(&rit)) {
|
|
EcsRest *rest = ecs_field(&rit, EcsRest, 1);
|
|
int i;
|
|
for (i = 0; i < rit.count; i ++) {
|
|
ecs_rest_ctx_t *ctx = rest[i].impl;
|
|
ecs_http_server_stop(ctx->srv);
|
|
}
|
|
}
|
|
} else if (it->event == EcsOnRemove) {
|
|
/* REST module was enabled */
|
|
while (ecs_term_next(&rit)) {
|
|
EcsRest *rest = ecs_field(&rit, EcsRest, 1);
|
|
int i;
|
|
for (i = 0; i < rit.count; i ++) {
|
|
ecs_rest_ctx_t *ctx = rest[i].impl;
|
|
ecs_http_server_start(ctx->srv);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FlecsRestImport(
|
|
ecs_world_t *world)
|
|
{
|
|
ECS_MODULE(world, FlecsRest);
|
|
|
|
ECS_IMPORT(world, FlecsPipeline);
|
|
#ifdef FLECS_PLECS
|
|
ECS_IMPORT(world, FlecsScript);
|
|
#endif
|
|
|
|
ecs_set_name_prefix(world, "Ecs");
|
|
|
|
flecs_bootstrap_component(world, EcsRest);
|
|
|
|
ecs_set_hooks(world, EcsRest, {
|
|
.ctor = ecs_default_ctor,
|
|
.move = ecs_move(EcsRest),
|
|
.copy = ecs_copy(EcsRest),
|
|
.dtor = ecs_dtor(EcsRest),
|
|
.on_set = flecs_on_set_rest
|
|
});
|
|
|
|
ECS_SYSTEM(world, DequeueRest, EcsPostFrame, EcsRest);
|
|
|
|
ecs_system(world, {
|
|
.entity = ecs_id(DequeueRest),
|
|
.no_readonly = true
|
|
});
|
|
|
|
ecs_observer(world, {
|
|
.filter = {
|
|
.terms = {{ .id = EcsDisabled, .src.id = ecs_id(FlecsRest) }}
|
|
},
|
|
.events = {EcsOnAdd, EcsOnRemove},
|
|
.callback = DisableRest
|
|
});
|
|
|
|
ecs_set_name_prefix(world, "EcsRest");
|
|
ECS_TAG_DEFINE(world, EcsRestPlecs);
|
|
}
|
|
|
|
#endif
|