replay/tsrtool: add a basic CLI replay editor

This commit is contained in:
Andrei Alexeyev 2023-06-13 06:23:33 +02:00
parent ba8ef49030
commit b7f47e6580
No known key found for this signature in database
GPG key ID: 72D26128040B9690
4 changed files with 421 additions and 2 deletions

View file

@ -34,6 +34,7 @@
#include "dynstage.h"
#include "eventloop/eventloop.h"
#include "replay/demoplayer.h"
#include "replay/tsrtool.h"
attr_unused
static void taisei_shutdown(void) {
@ -284,14 +285,20 @@ int main(int argc, char **argv);
attr_used
int main(int argc, char **argv) {
auto ctx = ALLOC(MainContext);
setlocale(LC_ALL, "C");
thread_init();
coroutines_init();
init_log();
stageinfo_init(); // cli_args depends on this
#if DEBUG
if(argc > 1 && !strcmp("tsrtool", argv[1])) {
return tsrtool_main(argc - 1, argv + 1);
}
#endif
auto ctx = ALLOC(MainContext);
// commandline arguments should be parsed as early as possible
cli_args(argc, argv, &ctx->cli); // stage_init_array goes first!

View file

@ -9,3 +9,9 @@ replay_src = files(
'state.c',
'write.c',
)
if is_developer_build
replay_src += files(
'tsrtool.c',
)
endif

394
src/replay/tsrtool.c Normal file
View file

@ -0,0 +1,394 @@
/*
* This software is licensed under the terms of the MIT License.
* See COPYING for further information.
* ---
* Copyright (c) 2011-2019, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2019, Andrei Alexeyev <akari@taisei-project.org>.
*/
#include "taisei.h"
#include "tsrtool.h"
#include "replay.h"
#include "struct.h"
#include "stage.h"
#include "util.h"
#include "util/strbuf.h"
#include "plrmodes.h"
typedef struct Command {
const char *name;
int min_args;
int (*func)(int argc, char **argv);
const char *usage;
} Command;
static struct {
Replay rpy;
uint16_t save_version;
ReplayStage *active_stage;
} G = {
.save_version = REPLAY_STRUCT_VERSION_WRITE,
};
static int cmd_reset(int argc, char **argv) {
replay_reset(&G.rpy);
G.active_stage = NULL;
return 0;
}
static int cmd_load(int argc, char **argv) {
replay_reset(&G.rpy);
if(!replay_load_syspath(&G.rpy, argv[1], REPLAY_READ_ALL | REPLAY_READ_IGNORE_ERRORS)) {
return -1;
}
return 1;
}
static int cmd_version(int argc, char **argv) {
++argv;
if(!strcmp(*argv, "default")) {
G.save_version = REPLAY_STRUCT_VERSION_WRITE;
} else {
char *endp = *argv;
G.save_version = strtol(*argv, &endp, 10);
while(isspace(*endp)) {
++endp;
}
if(strcmp(endp, "u")) {
G.save_version |= REPLAY_VERSION_COMPRESSION_BIT;
}
}
return 1;
}
static int cmd_save(int argc, char **argv) {
if(!replay_save_syspath(&G.rpy, argv[1], G.save_version)) {
return -1;
}
return 1;
}
#define ACTIVE_STAGE ({ \
auto _stg = G.active_stage; \
if(!_stg) { \
log_error("No stage selected"); \
return -1; \
} \
_stg; \
})
static int cmd_stage(int argc, char **argv) {
int n = strtol(argv[1], NULL, 10);
if(n >= G.rpy.stages.num_elements || n < 0) {
log_error("Stage number %i out of range; %i loaded", n, G.rpy.stages.num_elements);
return -1;
}
G.active_stage = dynarray_get_ptr(&G.rpy.stages, n);
return 1;
}
static bool filter_drop(const void *pelem, void *dropelem) {
return pelem != dropelem;
}
static int cmd_drop(int argc, char **argv) {
auto stg = ACTIVE_STAGE;
dynarray_filter(&G.rpy.stages, filter_drop, stg);
G.active_stage = NULL;
return 0;
}
static bool filter_isolate(const void *pelem, void *wantelem) {
return pelem == wantelem;
}
static int cmd_isolate(int argc, char **argv) {
auto stg = ACTIVE_STAGE;
dynarray_filter(&G.rpy.stages, filter_isolate, stg);
assert(G.rpy.stages.num_elements == 1);
G.active_stage = dynarray_get_ptr(&G.rpy.stages, 0);
return 0;
}
static int cmd_skip(int argc, char **argv) {
auto stg = ACTIVE_STAGE;
stg->skip_frames = strtol(argv[1], NULL, 10);
return 1;
}
static int cmd_trim(int argc, char **argv) {
auto stg = ACTIVE_STAGE;
int endframe = strtol(argv[1], NULL, 10);
dynarray_foreach(&stg->events, int i, ReplayEvent *evt, {
if(evt->frame >= endframe) {
stg->events.num_elements = i;
break;
}
});
replay_stage_event(stg, endframe, EV_OVER, 0);
stg->num_events = stg->events.num_elements;
return 1;
}
#define APPEND_FLAG_SEPARATOR() \
if(first) { \
first = false; \
} else { \
strbuf_cat(sbuf, " | "); \
}
#define APPEND_FLAG_NAME(name, bit) \
if(flags & (1 << bit)) { \
APPEND_FLAG_SEPARATOR() \
strbuf_cat(sbuf, #name); \
flags &= ~(1 << bit); \
} \
static void dump_gflags(StringBuffer *sbuf, uint32_t flags) {
bool first = true;
#define REPLAY_GFLAG(name, bit) \
APPEND_FLAG_NAME(REPLAY_GFLAG_##name, bit)
REPLAY_GFLAGS
#undef REPLAY_GFLAG
if(flags) {
APPEND_FLAG_SEPARATOR()
strbuf_printf(sbuf, "0x%X", flags);
}
}
static void dump_sflags(StringBuffer *sbuf, uint32_t flags) {
bool first = true;
#define REPLAY_SFLAG(name, bit) \
APPEND_FLAG_NAME(REPLAY_SFLAG_##name, bit)
REPLAY_SFLAGS
#undef REPLAY_SFLAG
if(flags) {
APPEND_FLAG_SEPARATOR()
strbuf_printf(sbuf, "0x%X", flags);
}
}
static void dump_inflags(StringBuffer *sbuf, uint32_t flags) {
bool first = true;
#define INFLAG(name, bit) \
APPEND_FLAG_NAME(INFLAG_##name, bit)
INFLAGS
#undef INFLAG
if(flags) {
APPEND_FLAG_SEPARATOR()
strbuf_printf(sbuf, "0x%X", flags);
}
}
#undef APPEND_FLAG_SEPARATOR
#undef APPEND_FLAG_NAME
static void flushline(StringBuffer *sbuf) {
fputs(sbuf->start, stdout);
fputc('\n', stdout);
strbuf_clear(sbuf);
}
static int cmd_info(int argc, char **argv) {
StringBuffer sbuf = {};
strbuf_printf(&sbuf,
"Struct version: %i %scompressed",
G.rpy.version & ~REPLAY_VERSION_COMPRESSION_BIT,
(G.rpy.version & REPLAY_VERSION_COMPRESSION_BIT) ? "" : "un"
);
flushline(&sbuf);
strbuf_cat(&sbuf, "Game version: ");
taisei_version_tostrbuf(&sbuf, &G.rpy.game_version);
flushline(&sbuf);
strbuf_printf(&sbuf, "Player: %s", G.rpy.playername);
flushline(&sbuf);
strbuf_printf(&sbuf, "Flags: 0x%04X : ", G.rpy.flags);
dump_gflags(&sbuf, G.rpy.flags);
flushline(&sbuf);
strbuf_printf(&sbuf, "Stages: %u", G.rpy.stages.num_elements);
flushline(&sbuf);
dynarray_foreach(&G.rpy.stages, int stgnum, ReplayStage *stg, {
char tmp[128];
strbuf_printf(&sbuf, "\nStage #%i", stgnum);
flushline(&sbuf);
strbuf_printf(&sbuf, "ID: 0x%04X", stg->stage);
flushline(&sbuf);
StageInfo *si = stageinfo_get_by_id(stg->stage);
if(si) {
strbuf_printf(&sbuf, "Title: %s - %s", si->title, si->subtitle);
} else {
strbuf_cat(&sbuf, "Title: <unknown>");
}
flushline(&sbuf);
PlayerMode *pm = plrmode_find(stg->plr_char, stg->plr_shot);
if(pm) {
plrmode_repr(tmp, sizeof(tmp), pm, false);
strbuf_printf(&sbuf, "Character: %s", tmp);
} else {
strbuf_printf(&sbuf,
"Character: Unknown (0x%02X, 0x%02X)", stg->plr_char, stg->plr_shot);
}
flushline(&sbuf);
strbuf_printf(&sbuf, "Difficulty: %s", difficulty_name(stg->diff));
flushline(&sbuf);
strbuf_printf(&sbuf, "Flags: 0x%04X : ", stg->flags);
dump_sflags(&sbuf, stg->flags);
flushline(&sbuf);
strbuf_printf(&sbuf, "Random seed: 0x%016llx", (unsigned long long)stg->rng_seed);
flushline(&sbuf);
strbuf_printf(&sbuf, "Skip frames: %u", stg->skip_frames);
flushline(&sbuf);
time_t t = stg->start_time;
struct tm *timeinfo = localtime(&t);
strftime(tmp, sizeof(tmp), "%Y-%m-%d %H:%M", timeinfo);
strbuf_printf(&sbuf, "Starting time: %s", tmp);
flushline(&sbuf);
strbuf_printf(&sbuf, "Starting position: (%i, %i)", stg->plr_pos_x, stg->plr_pos_y);
flushline(&sbuf);
strbuf_printf(&sbuf, "Starting power: %i", stg->plr_power);
flushline(&sbuf);
strbuf_printf(&sbuf, "Starting lives: %i, %i fragments", stg->plr_lives, stg->plr_life_fragments);
flushline(&sbuf);
strbuf_printf(&sbuf, "Starting bombs: %i, %i fragments", stg->plr_bombs, stg->plr_bomb_fragments);
flushline(&sbuf);
strbuf_printf(&sbuf, "Starting graze: %u", stg->plr_graze);
flushline(&sbuf);
strbuf_printf(&sbuf, "Starting PIV: %u", stg->plr_point_item_value);
flushline(&sbuf);
strbuf_printf(&sbuf, "Starting input: 0x%04X : ", stg->plr_inputflags);
dump_inflags(&sbuf, stg->plr_inputflags);
flushline(&sbuf);
strbuf_printf(&sbuf, "Starting total lives used: %u", stg->plr_total_lives_used);
flushline(&sbuf);
strbuf_printf(&sbuf, "Starting total bombs used: %u", stg->plr_total_bombs_used);
flushline(&sbuf);
strbuf_printf(&sbuf, "Starting total continues used: %u", stg->plr_total_continues_used);
flushline(&sbuf);
strbuf_printf(&sbuf, "Starting score: %llu", (unsigned long long)stg->plr_points);
flushline(&sbuf);
strbuf_printf(&sbuf, "Final score: %llu", (unsigned long long)stg->plr_points_final);
flushline(&sbuf);
strbuf_printf(&sbuf, "Final lives used (this stage): %u", stg->plr_stage_lives_used_final);
flushline(&sbuf);
strbuf_printf(&sbuf, "Final bombs used (this stage): %u", stg->plr_stage_bombs_used_final);
flushline(&sbuf);
strbuf_printf(&sbuf, "Final continues used (this stage): %u", stg->plr_stage_continues_used_final);
flushline(&sbuf);
strbuf_printf(&sbuf, "Events: %u", stg->events.num_elements);
flushline(&sbuf);
});
strbuf_free(&sbuf);
return 0;
}
static Command commands[] = {
{ "reset", 0, cmd_reset, },
{ "load", 1, cmd_load, "load <filename.tsr>" },
{ "save", 1, cmd_save, "save <filename.tsr>" },
{ "version", 1, cmd_version, "version <default>|(<version>[u])" },
{ "stage", 1, cmd_stage, "stage <num>" },
{ "skip", 1, cmd_skip, "skip <numframes>" },
{ "trim", 1, cmd_trim, "trim <numframes>" },
{ "drop", 0, cmd_drop, },
{ "isolate", 0, cmd_isolate, },
{ "info", 0, cmd_info, },
};
int tsrtool_main(int argc, char **argv) {
char **end = argv + argc;
++argv;
while(argv < end) {
bool found = false;
for(int i = 0; i < ARRAY_SIZE(commands); ++i) {
if(!strcmp(*argv, commands[i].name)) {
found = true;
if(end - argv - 1 < commands[i].min_args) {
log_error(
"Not enough arguments to '%s'. Usage: %s",
*argv, commands[i].usage
);
return 2;
}
int result = commands[i].func(end - argv, argv);
if(result < 0) {
return -result;
}
argv += 1 + result;
break;
}
}
if(!found) {
log_error("No such command: %s", *argv);
return 3;
}
}
return 0;
}

12
src/replay/tsrtool.h Normal file
View file

@ -0,0 +1,12 @@
/*
* This software is licensed under the terms of the MIT License.
* See COPYING for further information.
* ---
* Copyright (c) 2011-2019, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2019, Andrei Alexeyev <akari@taisei-project.org>.
*/
#pragma once
#include "taisei.h"
int tsrtool_main(int argc, char **argv);