diff --git a/rectests/SHOULDFAIL.REC b/rectests/SHOULDFAIL.REC new file mode 100644 index 000000000..dd8354705 Binary files /dev/null and b/rectests/SHOULDFAIL.REC differ diff --git a/run_rectests.sh b/run_rectests.sh index 5029922eb..4b38e395d 100755 --- a/run_rectests.sh +++ b/run_rectests.sh @@ -6,77 +6,13 @@ if [ -z "$1" ]; then fi export BUILD_DIR="$1" -OPENOMF_BIN=$(find "$BUILD_DIR" -name openomf -type f -executable -print -quit) +OPENOMF_BIN=$(find "$BUILD_DIR" "(" -name openomf -or -name openomf.exe ")" -type f -executable -print -quit) if [ -z "$OPENOMF_BIN" ]; then echo "Could not find openomf executable from $BUILD_DIR" >&2 exit 1 fi export OPENOMF_BIN="./${OPENOMF_BIN#$BUILD_DIR}" -# Define your tests here (description:filename) -tests=( - "There is a 3 tick delay between jumps:JUMP_DELAY.REC" - "Overhead throw should grab opponent on the first tick of anim 22, throw opponent to the right, and not be interrupted by the ground:JAG-THROW-LAND.REC" - "Electra should be able to kick while doing inputs for rolling thunder:65K6P.REC" - "Jaguar uses same animation for K and 6P but Shadow does not:6P.REC" - "Novas 1P can hit more than once with the Q tag and use AI for air launch:NOVA-1P.REC" - "Jaguar standing throw should not result in overlap with opponent in corner:JAG-CORNER-THROW.REC" - "Jaguar doing a standing throw as the winning move should not cause a slide:JAG-NO-SLIDE-WIN-THROW.REC" - "Flail charging punch should cancel when blocked:FLAIL_454P.REC" - "Jaguar should keep walking right after doing a standing throw if the input is held:KEEP-WALKING-AFTER-JAG-STANDING-THROW.REC" - "Jaguar should be able to walk underneath Gargoyle:WALK_UNDERNEATH.REC" - "Test Jaguar destruction:JAG-DESTRUCT.REC" - "Kreissack has a custom defeat animation:KREISSACK.REC" - "Shadow grab should work in the middle and the corners of arena:SHADOW-GRAB.REC" - "Projectiles should do knockback:PROJECTILE-KNOCKBACK.REC" - "Enemy should recover from air hits unless it is a KO:AIR-HIT.REC" - "Only some moves should wallslam:STADIUM-WALL-SPLAT.REC" - "Power Plant with hazards enabled has a lower wallslam tolerance:POWERPLANT-WALL-SPLAT.REC" - "HARs in stasis cannot hit opponent:STASISED-HARS-CANNOT-HIT.REC" - "HARs play ANIM_DEFEAT instead of idle:NO-IDLE-DEFEAT.REC" - "Electra P656 cannot be interrupted during anim 36:ELECTRA-MOVE36-EMFLAG.REC" - "Chronos matter phasing (14K) noclips through both walls:CHRONOS-WALLNOCLIP.REC" - "Chronos can control teleport by holding left/right:CHRONOS_TELEPORT_CONTROL.REC" - "Electra Extended Rolling Thunder plays anim 40 upon hitting the wall:ELECTRA-EXROLLTHNDR-WALLBOUNCE.REC" - "Katana Wall Spin (14K) cannot be interrupted:KATANA-WALLSPIN-NOINTERRUPT.REC" - "Being kicked in the back of the head by chronos matter phasing knocks you forwards, flipping you:KNOCKBACK-DIR.REC" - "While walking, face your jumping enemy:FACE-JUMPING-ENEMY.REC" - "Nova's grenade should not fly over Electra's head:NOVA-GRENADE.REC" - "Buffering 2 (DOWN) 6 ticks before jumping grants a SUPER JUMP. Only the first two jumps should be super.:SUPERJUMP.REC" - "Buffered inputs are only used to match moves if the direction has changed in the last 9 ticks:INPUT-FRESHNESS.REC" - "Stasis prevents walking, turning, and endurance regen:STASIS-FREEZES.REC" - "Electra's shards can be blocked:SHARDS-ARE-BLOCKABLE.REC" - "Electra can throw Katana on wakeup without getting hit:WAKEUP_THROW.REC" - "Katana can chain stomps and end with a jump kick:STOMP.REC" - "Gargoyle's diving claw doesn't flip the opponent around:DIVING-CLAW.REC" - "Flail cannot hit air 66K throw out of range:THROW-RANGE.REC" - "Flail cannot hit 66K if the followup animation is out of range:THROW_RANGE_BUG.REC" - "Flail cannot be interrupted by hazards after throwing:THROW_INTERRUPT.REC" - "Flail's 66K attempt KO's the opponent and the game is fine with it:THROW_KO.REC" - "Throwing a stunned opponent resets their stun bar:STUN_THROW.REC" - "Gargoyle reaches the top of the stage:FLY.REC" - "Shadow grab can be interrupted by hitting the originating HAR:SHADOW-GRAB-INTERRUPT-HIT.REC" - "Jaguar throw deals half stun due to rehit rules:DAMPENED_STUN.REC" - "Pyros's flames cannot be hit:PYROS_PRIORITY.REC" - "Flail's destruct sequence works completely:FLAIL_DESTRUCT.REC" - "Electra is stuck blocking until Thorn's ul tag wears off:UL_TAG_HOLD_UP.REC" - "Pyros's destruct sequence works completely:PYROS_DESTRUCT.REC" - "Katana's corkscrew blade has landing recovery:LANDING_RECOVERY.REC" - "Flail 66K is unblockable:UNBLOCKABLE_THROW.REC" - "Electra 66P dodges Earthquake Smash:DODGE_EARTHQUAKE.REC" - "Katana's extended rising blade deals the correct amount of damage:KATANA_DAMAGE.REC" - "Enemies have invuln after recovering from an air hit:AIR_IMMUNITY_NO_REHIT.REC" - "Enemies cannot be juggled by the same move:AIR_IMMUNITY_REHIT.REC" - "Jaguar's Bread And Butter throw combo:REHIT_COMBO.REC" - "Jaguar cannot pick up the opponent off the ground:WAKEUP_THROW_PROTECTION.REC" - "Players cannot blocking while attacking:BLOCKING_INTERRUPT.REC" - "Katana cannot infinitely heel stomp to the moon with rehit mode:SPACE_JUMP.REC" - "Katana can turn around after heel stomp connects:KATANA_STOMP_TURN.REC" - "Nova can combo a stunned HAR from belly flop and enemy continues to be stunned:NOVA_FLOP_STUN_COMBO.REC" - "Rehit juggles are not allowed against stunned opponents:NO_STUN_JUGGLE.REC" - "Katana enters winpose after landing from heel stomp:KATANA_WINPOSE.REC" -) - # Setup temp directory for outputs temp_dir=$(mktemp -d) trap 'rm -rf "$temp_dir"' EXIT @@ -96,31 +32,34 @@ RUNDIR=$(pwd) cd $BUILD_DIR +export OPENOMF_RESOURCE_PATH="." export LSAN_OPTIONS="suppressions=../lsan.supp" -i=0 -for test in "${tests[@]}"; do - IFS=':' read -r desc filename <<< "$test" - # Trim whitespace from description and filename - desc=$(echo "$desc" | sed -re 's/^[[:blank:]]+|[[:blank:]]+$//g') - filename=$(echo "$filename" | sed -re 's/^[[:blank:]]+|[[:blank:]]+$//g') - output_file="$temp_dir/output_$i.log" - echo -n "${desc} :" - if $OPENOMF_BIN --force-audio-backend=NULL --force-renderer=NULL --speed=10 -P "$RUNDIR/rectests/${filename}" >"$output_file" 2>&1; then - echo " PASS" - else - echo " FAILED (${filename})" - fail_summary="${fail_summary} ${filename}" - cat $output_file - ((fail_count++)) +output_file="$temp_dir/output_shouldfail.log" +if $OPENOMF_BIN --force-audio-backend=NULL --force-renderer=NULL --warp -P "$RUNDIR/rectests/SHOULDFAIL.REC" >"$output_file" 2>&1; then + cat "$output_file" + echo "CRITICAL ERROR: SHOULDFAIL.REC succeeded." + exit 1 +fi + +cmd="\$OPENOMF_BIN --force-audio-backend=NULL --force-renderer=NULL --warp" + +for filename in "$RUNDIR"/rectests/*.REC; do + if [[ "$filename" == *SHOULDFAIL.REC ]]; then + continue fi - ((i++)) + cmd="$cmd -P '$filename'" + found_something=1 done -if [ $fail_count -ne 0 ]; then - fail_summary="Failed ${fail_count} tests:${fail_summary}" - echo "${fail_summary}" +if [ -z "${found_something}" ]; then + echo "Failed to find any rectests!" + exit 1 +fi - # Exit with non-zero status if any test failed - exit $fail_count +if RUNDIR="${RUNDIR}" OPENOMF_BIN="${OPENOMF_BIN}" sh -c "$cmd"; then + echo "ALL PASSED." +else + echo "RECTESTS FAILED, GOODBYE." + exit 1 fi diff --git a/src/controller/rec_controller.c b/src/controller/rec_controller.c index 1f1d96aaf..d38b3446d 100644 --- a/src/controller/rec_controller.c +++ b/src/controller/rec_controller.c @@ -88,7 +88,19 @@ int rec_controller_poll(controller *ctrl, ctrl_event **ev) { if(parse_assertion(buf, &ass)) { log_assertion(&ass); if(!game_state_check_assertion_is_met(&ass, ctrl->gs)) { - crash("REC file assert failed!"); + if(ctrl->gs->init_flags->playback <= 1) { + // no need to continue, we're only playing a single rec. + crash("RECTEST Failed."); + } + // batch rectest + str ass_str; + rec_assertion_to_str(&ass_str, &ass); + char const *rec_filename = path_c(&ctrl->gs->init_flags->rec_files[ctrl->gs->rec_playback_id]); + str_append_format(&ctrl->gs->rectest_failures, + "REC Assertion Failed!\n File: %s\n %s (tick %d)\n", rec_filename, + str_c(&ass_str), ticks); + controller_close(ctrl, ev); + return 0; } } } else if(move->lookup_id == 96) { @@ -169,7 +181,6 @@ void rec_controller_step_back(controller *ctrl) { game_state *gs_new = omf_calloc(1, sizeof(game_state)); game_state_clone(gs_bak, gs_new); - gs_new->clone = false; ctrl->gs->new_state = gs_new; data->last_tick = ctrl->gs->tick; diff --git a/src/engine.c b/src/engine.c index 19c1e90a5..45ce781ae 100644 --- a/src/engine.c +++ b/src/engine.c @@ -210,14 +210,8 @@ void engine_run(engine_init_flags *init_flags) { if(game_state_get_player(gs, 0)->ctrl->type == CTRL_TYPE_REC) { controller_rewind(game_state_get_player(gs, 0)->ctrl); controller_rewind(game_state_get_player(gs, 1)->ctrl); - if(gs->new_state) { - // one of the controllers wants to replace the game state - game_state *old_gs = gs; - game_state *new_gs = gs->new_state; - gs = new_gs; - game_state_clone_free(old_gs); - omf_free(old_gs); - } + // check if we need to replace the game state + game_state_check_for_new(&gs); visual_debugger = 1; } } @@ -328,15 +322,7 @@ void engine_run(engine_init_flags *init_flags) { if(has_static) { game_state_static_tick(gs, false); // check if we need to replace the game state - if(gs->new_state) { - // one of the controllers wants to replace the game state - game_state *old_gs = gs; - game_state *new_gs = gs->new_state; - gs = new_gs; - // gs->new_state = NULL; - game_state_clone_free(old_gs); - omf_free(old_gs); - } + game_state_check_for_new(&gs); console_tick(gs); static_wait -= STATIC_TICKS; } @@ -347,6 +333,8 @@ void engine_run(engine_init_flags *init_flags) { has_dynamic = dynamic_wait > dyntick_ms; if(has_dynamic) { game_state_dynamic_tick(gs, false); + // check if we need to replace the game state + game_state_check_for_new(&gs); dynamic_wait -= dyntick_ms; if(gs->delay > 0) { log_debug("applying delay %d", gs->delay); diff --git a/src/engine.h b/src/engine.h index 887b605a9..b982489e1 100644 --- a/src/engine.h +++ b/src/engine.h @@ -12,7 +12,7 @@ typedef struct engine_init_flags { unsigned int playback; char force_renderer[16]; char force_audio_backend[16]; - path rec_file; + path *rec_files; int warpspeed; int speed; } engine_init_flags; diff --git a/src/game/game_state.c b/src/game/game_state.c index e14059fab..23321bd34 100644 --- a/src/game/game_state.c +++ b/src/game/game_state.c @@ -319,17 +319,19 @@ int game_state_create(game_state *gs, engine_init_flags *init_flags) { reconfigure_controller(gs); int nscene; - if(path_is_set(&init_flags->rec_file) > 0 && init_flags->playback == 1) { + if(init_flags->playback > 0 && gs->rec_playback_id < gs->init_flags->playback && + path_is_set(&init_flags->rec_files[gs->rec_playback_id])) { + path const *rec = &init_flags->rec_files[gs->rec_playback_id]; gs->rec = omf_malloc(sizeof(sd_rec_file)); sd_rec_create(gs->rec); - int ret = sd_rec_load(gs->rec, &init_flags->rec_file); + int ret = sd_rec_load(gs->rec, rec); if(ret != SD_SUCCESS) { - log_error("Unable to load recording %s.", path_c(&init_flags->rec_file)); + log_error("Unable to load recording %s.", path_c(rec)); goto error_0; } nscene = SCENE_ARENA0 + gs->rec->arena_id; - log_debug("playing recording file %s", path_c(&init_flags->rec_file)); + log_debug("playing recording file %s", path_c(rec)); gs->this_id = nscene; gs->next_id = nscene; @@ -402,6 +404,8 @@ int game_state_create(game_state *gs, engine_init_flags *init_flags) { // Initialize scene scene_init(gs->sc); + str_create(&gs->rectest_failures); + // All done return 0; @@ -1035,6 +1039,9 @@ void game_state_static_tick(game_state *gs, bool replay) { if(gs->next_id == SCENE_NONE) { log_debug("Next ID is SCENE_NONE! bailing."); gs->run = 0; + if(str_size(&gs->rectest_failures) > 0) { + crash_with_args("RECTEST(s) FAILED:\n%s", str_c(&gs->rectest_failures)); + } return; } @@ -1059,8 +1066,6 @@ void game_state_static_tick(game_state *gs, bool replay) { // merge the sounds game_state_merge_sounds(gs, gs->new_state); gs = gs->new_state; - // remove the cloned flag - gs->clone = false; } // Call static ticks for scene @@ -1314,6 +1319,7 @@ void game_state_menu_poll(game_state *gs, ctrl_event **ev) { } void game_state_clone_free(game_state *gs) { + assert(gs->clone); // Free objects render_obj *robj; iterator it; @@ -1330,19 +1336,59 @@ void game_state_clone_free(game_state *gs) { scene_clone_free(gs->sc); // omf_free(gs->sc); + if(gs->rec) { + for(int i = 0; i < 2; i++) { + if(gs->players[i]->pilot == &gs->rec->pilots[i].info) { + gs->players[i]->pilot = NULL; + } + } + } + // Free players for(int i = 0; i < 2; i++) { // game_player_set_ctrl(gs->players[i], NULL); game_player_clone_free(gs->players[i]); omf_free(gs->players[i]); } + str_free(&gs->rectest_failures); // omf_free(gs); } +void game_state_swap_cloneness(game_state *gs1, game_state *gs2) { + assert(gs1 != gs2); + // only one of gs1 / gs2 can be a clone. + assert((gs1->clone && !gs2->clone) || (!gs1->clone && gs2->clone)); + // these pointers are not deep-cloned. + assert(gs1->rec == gs2->rec); + assert(gs1->menu_ctrl == gs2->menu_ctrl); + + bool tmp = gs1->clone; + gs1->clone = gs2->clone; + gs2->clone = tmp; +} + +void game_state_check_for_new(game_state **_gs) { + game_state *gs = *_gs; + if(gs->new_state) { + // somebody wants to replace the game state + game_state *old_gs = gs; + game_state *new_gs = gs->new_state; + *_gs = new_gs; + if(new_gs->clone) { + game_state_swap_cloneness(old_gs, new_gs); + game_state_clone_free(old_gs); + omf_free(old_gs); + } else { + game_state_free(&old_gs); + } + } +} void game_state_free(game_state **_gs) { game_state *gs = *_gs; *_gs = NULL; + assert(!gs->clone); + // Free objects render_obj *robj; iterator it; @@ -1362,15 +1408,15 @@ void game_state_free(game_state **_gs) { } if(gs->rec) { + for(int i = 0; i < 2; i++) { + if(gs->players[i]->pilot == &gs->rec->pilots[i].info) { + gs->players[i]->pilot = NULL; + } + } sd_rec_free(gs->rec); omf_free(gs->rec); } - if(gs->init_flags->playback == 1) { - gs->players[0]->pilot = NULL; - gs->players[1]->pilot = NULL; - } - // Free players for(int i = 0; i < 2; i++) { game_player_set_ctrl(gs->players[i], NULL); @@ -1378,6 +1424,7 @@ void game_state_free(game_state **_gs) { omf_free(gs->players[i]); } omf_free(gs->menu_ctrl); + str_free(&gs->rectest_failures); omf_free(gs); } @@ -1510,6 +1557,8 @@ int game_state_clone(game_state *src, game_state *dst) { dst->sc = omf_calloc(1, sizeof(scene)); scene_clone(src->sc, dst->sc, dst); + str_from(&dst->rectest_failures, &src->rectest_failures); + dst->new_state = NULL; dst->clone = true; @@ -1523,13 +1572,44 @@ bool game_state_hars_are_alive(game_state *gs) { return (h1->health > 0) && (h2->health > 0); } +void game_state_rec_finished(game_state *gs) { + assert(is_rec_playback(gs)); + + unsigned int const next_rec_id = gs->rec_playback_id + 1; + if(next_rec_id >= gs->init_flags->playback) { + if(gs->next_wait_ticks <= 0) { + log_warn("Finished playing %d RECs.", gs->init_flags->playback); + } + game_state_set_next(gs, SCENE_NONE); + return; + } + + if(gs->new_state != NULL) + return; + + game_state *new_gs = omf_calloc(1, sizeof(game_state)); + new_gs->rec_playback_id = next_rec_id; + if(game_state_create(new_gs, gs->init_flags) != 0) { + game_state_free(&new_gs); + log_warn("Failed to create game_state for %dth (nth) rec\n", next_rec_id + 1); + game_state_set_next(gs, SCENE_NONE); + return; + } + + str_free(&new_gs->rectest_failures); + memcpy(&new_gs->rectest_failures, &gs->rectest_failures, sizeof(str)); + str_create(&gs->rectest_failures); + + gs->new_state = new_gs; +} + bool is_netplay(const game_state *gs) { return game_state_get_player(gs, 0)->ctrl->type == CTRL_TYPE_NETWORK || game_state_get_player(gs, 1)->ctrl->type == CTRL_TYPE_NETWORK; } bool is_singleplayer(const game_state *gs) { - if(gs->rec && gs->init_flags->playback == 1 && gs->rec->p2_controller == REC_CONTROLLER_AI) { + if(gs->rec && is_rec_playback(gs) && gs->rec->p2_controller == REC_CONTROLLER_AI) { return true; } return game_state_get_player(gs, 1)->ctrl->type == CTRL_TYPE_AI; @@ -1556,5 +1636,6 @@ bool is_twoplayer(const game_state *gs) { } bool is_rec_playback(const game_state *gs) { - return path_is_set(&gs->init_flags->rec_file) && gs->init_flags->playback == 1; + return gs->rec_playback_id < gs->init_flags->playback && + path_is_set(&gs->init_flags->rec_files[gs->rec_playback_id]); } diff --git a/src/game/game_state.h b/src/game/game_state.h index 594a9aa46..b59946bc9 100644 --- a/src/game/game_state.h +++ b/src/game/game_state.h @@ -45,6 +45,7 @@ void game_state_init_demo(game_state *gs); int game_state_ms_per_dyntick(game_state *gs); ticktimer *game_state_get_ticktimer(game_state *gs); bool game_state_hars_are_alive(game_state *gs); +void game_state_rec_finished(game_state *gs); object *game_state_find_object(game_state *gs, uint32_t object_id); int game_state_find_objects(game_state *gs, vector *out, bool (*predicate)(const object *obj, void *user_data), @@ -55,6 +56,8 @@ void game_state_play_sound(game_state *gs, int id, float volume, float panning, int game_state_clone(game_state *src, game_state *dst); void game_state_clone_free(game_state *gs); +void game_state_swap_cloneness(game_state *gs1, game_state *gs2); +void game_state_check_for_new(game_state **gs); void _setup_keyboard(game_state *gs, int player_id, int control_id); void _setup_ai(game_state *gs, int player_id); diff --git a/src/game/game_state_type.h b/src/game/game_state_type.h index df9629169..92bc164c7 100644 --- a/src/game/game_state_type.h +++ b/src/game/game_state_type.h @@ -64,6 +64,7 @@ typedef struct game_state_t { uint32_t int_tick; // never adjusted, used in ping calculation unsigned int role; unsigned int speed; + unsigned int rec_playback_id; engine_init_flags *init_flags; match_settings match_settings; @@ -105,6 +106,8 @@ typedef struct game_state_t { sd_rec_file *rec; controller *menu_ctrl; + + str rectest_failures; } game_state; #endif // GAME_STATE_TYPE_H diff --git a/src/game/scenes/arena.c b/src/game/scenes/arena.c index 6e147c623..52d5d79d3 100644 --- a/src/game/scenes/arena.c +++ b/src/game/scenes/arena.c @@ -124,7 +124,7 @@ void game_menu_quit(component *c, void *userdata) { chr_score_reset(game_player_get_score(game_state_get_player((s)->gs, 1)), 1); game_player *player1 = game_state_get_player(((scene *)userdata)->gs, 0); - if(s->gs->init_flags->playback == 1) { + if(s->gs->init_flags->playback) { // 'quit' button exits during REC playback game_state_set_next(s->gs, SCENE_NONE); } else if(player1->chr) { @@ -335,9 +335,8 @@ static void arena_end(scene *sc) { } // Switch scene - if(scene->gs->init_flags->playback == 1) { - // exit after REC playback - game_state_set_next(scene->gs, SCENE_NONE); + if(is_rec_playback(gs)) { + game_state_rec_finished(gs); } else if(is_singleplayer(gs) || is_tournament(gs) || is_demoplay(gs)) { game_player *p1 = game_state_get_player(gs, 0); game_player *p2 = game_state_get_player(gs, 1); @@ -1000,7 +999,7 @@ int arena_handle_events(scene *scene, game_player *player, ctrl_event *i) { } } else if(i->type == EVENT_TYPE_CLOSE) { if(player->ctrl->type == CTRL_TYPE_REC) { - game_state_set_next(scene->gs, SCENE_NONE); + game_state_rec_finished(scene->gs); } else { if(scene->gs->net_mode == NET_MODE_LOBBY) { arena_local *local = scene_get_userdata(scene); @@ -1610,7 +1609,7 @@ static void arena_free(scene *scene) { if(scene->gs->init_flags->record == 1) { // we're supposed to save it - sd_rec_save(scene->gs->rec, &scene->gs->init_flags->rec_file); + sd_rec_save(scene->gs->rec, &scene->gs->init_flags->rec_files[0]); } } @@ -1712,7 +1711,7 @@ int arena_create(scene *scene) { // TODO: Fire & Ice will need to set the arena palette unsigned pal_index = 0; - if(scene->gs->init_flags->playback == 1) { + if(is_rec_playback(scene->gs)) { // use palette index from rec pal_index = scene->gs->rec->arena_palette; } else if(scene->bk_data->file_id == 128 && is_tournament(scene->gs)) { diff --git a/src/main.c b/src/main.c index bef668883..631667620 100644 --- a/src/main.c +++ b/src/main.c @@ -58,6 +58,7 @@ int main(int argc, char *argv[]) { char *lobbyaddr = NULL; char *oldlobbyaddr = NULL; char *trace_file = NULL; + int retval = 1; unsigned short connect_port = 0; unsigned short listen_port = 0; engine_init_flags init_flags; @@ -75,7 +76,7 @@ int main(int argc, char *argv[]) { struct arg_str *force_renderer = arg_str0(NULL, "force-renderer", "", "Force a renderer to use"); struct arg_str *trace = arg_str0("t", "trace", "", "Trace netplay events to file"); struct arg_int *port = arg_int0("p", "port", "", "Port to connect or listen (default: 2097)"); - struct arg_file *play = arg_file0("P", "play", "", "Play an existing recfile"); + struct arg_file *play = arg_filen("P", "play", "", 0, 999, "Play an existing recfile"); struct arg_file *rec = arg_file0("R", "rec", "", "Record a new recfile"); struct arg_lit *warp = arg_lit0(NULL, "warp", "run the game at warp speed"); struct arg_int *speed = arg_int0(NULL, "speed", "", "game speed to use: 1-10"); @@ -135,11 +136,15 @@ int main(int argc, char *argv[]) { } else if(lobby->count > 0) { init_flags.net_mode = NET_MODE_LOBBY; } else if(play->count > 0) { - init_flags.playback = 1; - path_from_c(&init_flags.rec_file, play->filename[0]); + init_flags.playback = play->count; + init_flags.rec_files = omf_malloc(play->count * sizeof(path)); + for(int i = 0; i < play->count; i++) { + path_from_c(&init_flags.rec_files[i], play->filename[i]); + } } else if(rec->count > 0) { init_flags.record = 1; - path_from_c(&init_flags.rec_file, rec->filename[0]); + init_flags.rec_files = omf_malloc(1 * sizeof(path)); + path_from_c(&init_flags.rec_files[0], rec->filename[0]); } if(warp->count > 0) { @@ -285,6 +290,7 @@ int main(int argc, char *argv[]) { // Run engine_run(&init_flags); + retval = 0; // Close everything engine_close(); @@ -303,6 +309,9 @@ int main(int argc, char *argv[]) { log_info("Exit."); log_close(); exit_0: + if(init_flags.rec_files) { + omf_free(init_flags.rec_files); + } if(ip) { omf_free(ip); } @@ -313,5 +322,5 @@ int main(int argc, char *argv[]) { omf_free(trace_file); } arg_freetable(argtable, N_ELEMENTS(argtable)); - return 0; + return retval; }