stageinfo,stage: implement stage hot-reloading

POSIX platforms only. Must be enabled via -Dstages_live_reload=true.
This commit is contained in:
Andrei Alexeyev 2022-01-28 18:03:22 +02:00
parent 1260dcc0c3
commit dcb630b37d
No known key found for this signature in database
GPG key ID: 72D26128040B9690
17 changed files with 652 additions and 77 deletions

View file

@ -480,6 +480,17 @@ endif
version_deps = []
bindist_deps = []
stages_live_reload = get_option('stages_live_reload')
if stages_live_reload
assert(have_posix and host_machine.system() != 'emscripten',
'Stage live reloading is not supported on this platform')
endif
use_testing_stages = is_developer_build
config.set('TAISEI_BUILDCONF_DYNSTAGE', stages_live_reload)
config.set('TAISEI_BUILDCONF_TESTING_STAGES', use_testing_stages)
subdir('misc')
subdir('emscripten')
subdir('switch')
@ -520,6 +531,7 @@ Summary:
System type: @1@
Build type: @2@
Developer mode: @15@
Stages live reload: @16@
Audio backends: @3@ (default: @4@)
Renderers: @5@ (default: @6@)
@ -551,7 +563,8 @@ Summary:
join_paths('$prefix', bindir),
join_paths('$prefix', data_path),
join_paths('$prefix', doc_path),
is_developer_build
is_developer_build,
stages_live_reload
)
message(summary)

View file

@ -190,3 +190,10 @@ option(
value : false,
description : 'Build shaderc and spirv-cross CLI tools from subprojects even if system versions exist'
)
option(
'stages_live_reload',
type : 'boolean',
value : 'false',
description : 'Enable live-reloading workflow for stages (for development only)'
)

8
scripts/format-array.py Executable file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env python3
import sys
def quote(s):
return '"' + s.replace('\\', '\\\\').replace('"', '\\"') + '"'
print("{", ', '.join([quote(s) for s in sys.argv[1:]] + ['NULL']), "}")

View file

@ -152,3 +152,6 @@ em_set_bundle_uuid_command = [python_thunk, em_set_bundle_uuid_script]
fix_path_script = files('unfuck-path.py')
fix_path_command = [python_thunk, fix_path_script]
format_array_script = files('format-array.py')
format_array_command = [python_thunk, format_array_script]

View file

@ -18,6 +18,7 @@
#include "portrait.h"
#include "stages/stage5/stage5.h" // for unlockable bonus BGM
#include "stageobjects.h"
#include "dynstage.h"
static void ent_draw_boss(EntityInterface *ent);
static DamageResult ent_damage_boss(EntityInterface *ent, const DamageInfo *dmg);
@ -1001,6 +1002,13 @@ static void boss_call_rule(Boss *boss, int t) {
}
}
static bool spell_is_overload(AttackInfo *spell) {
// HACK HACK HACK
StagesExports *e = dynstage_get_exports();
struct stage5_spells_s *stage5_spells = (struct stage5_spells_s*)e->stage5.spells;
return spell == &stage5_spells->extra.overload;
}
void boss_finish_current_attack(Boss *boss) {
AttackType t = boss->current->type;
@ -1024,7 +1032,7 @@ void boss_finish_current_attack(Boss *boss) {
}
// HACK
if(boss->current->info == &stage5_spells.extra.overload) {
if(spell_is_overload(boss->current->info)) {
stage_unlock_bgm("bonus0");
}
} else if(boss->current->type != AT_ExtraSpell) {

315
src/dynstage.c Normal file
View file

@ -0,0 +1,315 @@
/*
* 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"
#ifndef TAISEI_BUILDCONF_HAVE_POSIX
#error Stage hot reloading is only supported on POSIX systems
#endif
#include "dynstage.h"
#include "events.h"
#include "filewatch/filewatch.h"
#include <dirent.h>
#include <dlfcn.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#define HT_SUFFIX fwatchset
#define HT_KEY_TYPE FileWatch*
#define HT_VALUE_TYPE struct ht_empty
#define HT_FUNC_HASH_KEY(key) htutil_hashfunc_uint64((uintptr_t)(key))
#define HT_FUNC_COPY_KEY(dst, src) (*(dst) = (FileWatch*)(src))
#define HT_KEY_FMT "p"
#define HT_KEY_PRINTABLE(key) (key)
#define HT_VALUE_FMT "i"
#define HT_VALUE_PRINTABLE(val) (1)
#define HT_KEY_CONST
#define HT_DECL
#define HT_IMPL
#include "hashtable_incproxy.inc.h"
typedef void *stageslib_t;
// in frames
#define RECOMPILE_DELAY 6
#define RESCAN_DELAY 6
#define LIBRARY_BUMP_DELAY 6
static struct {
stageslib_t lib;
StagesExports *exports;
FileWatch *lib_watch;
uint32_t lib_generation;
uint32_t src_generation;
ht_fwatchset_t src_watches;
bool monitoring_initialized;
int recompile_delay;
int rescan_delay;
int lib_gen_bump_delay;
pid_t recompile_pid;
} dynstage;
static stageslib_t dynstage_dlopen(void) {
stageslib_t lib = dlopen(TAISEI_BUILDCONF_DYNSTAGE_LIB, RTLD_NOW | RTLD_LOCAL);
if(UNLIKELY(!lib)) {
log_fatal("Failed to load stages library: %s", dlerror());
}
return lib;
}
static void dynstage_bump_lib_generation(void) {
// Delay reporting library update to avoid trying to load a partially written file
dynstage.lib_gen_bump_delay = imax(dynstage.lib_gen_bump_delay, LIBRARY_BUMP_DELAY);
}
static void dynstage_bump_src_generation(bool deleted) {
++dynstage.src_generation;
dynstage.recompile_delay = imax(dynstage.recompile_delay, RECOMPILE_DELAY);
if(deleted) {
// Some text editors may delete the original file and replace it when saving.
// In case that happens, rescan shortly after detecting a delete.
// Not very efficient, but simple since we don't have to track file names.
dynstage.rescan_delay = imax(dynstage.rescan_delay, RESCAN_DELAY);
}
log_debug("%i", dynstage.src_generation);
}
static void unwatch_sources(void);
static void watch_sources(void);
static void recompile(void);
static bool dynstage_frame_event(SDL_Event *e, void *ctx) {
if(!dynstage.lib_watch) {
dynstage.lib_watch = filewatch_watch(TAISEI_BUILDCONF_DYNSTAGE_LIB);
if(dynstage.lib_watch) {
log_debug("Created watch for %s", TAISEI_BUILDCONF_DYNSTAGE_LIB);
dynstage_bump_lib_generation();
}
}
if(dynstage.rescan_delay > 0 && --dynstage.rescan_delay == 0) {
unwatch_sources();
watch_sources();
}
if(dynstage.recompile_delay > 0 && --dynstage.recompile_delay == 0) {
recompile();
}
if(dynstage.lib_gen_bump_delay > 0 && --dynstage.lib_gen_bump_delay == 0) {
++dynstage.lib_generation;
log_debug("Bumped library generation: %i", dynstage.lib_generation);
}
return false;
}
static bool is_src_watch(FileWatch *w) {
return ht_fwatchset_lookup(&dynstage.src_watches, w, NULL);
}
static bool dynstage_filewatch_event(SDL_Event *e, void *ctx) {
FileWatch *watch = NOT_NULL(e->user.data1);
FileWatchEvent fevent = e->user.code;
if(watch == dynstage.lib_watch) {
switch(fevent) {
case FILEWATCH_FILE_UPDATED:
dynstage_bump_lib_generation();
break;
case FILEWATCH_FILE_DELETED:
filewatch_unwatch(dynstage.lib_watch);
dynstage.lib_watch = NULL;
break;
}
return false;
}
if(is_src_watch(watch)) {
dynstage_bump_src_generation(fevent == FILEWATCH_FILE_DELETED);
}
return false;
}
static void dynstage_sigchld(int sig, siginfo_t *sinfo, void *ctx) {
pid_t cpid;
int status;
while((cpid = waitpid(-1, &status, WNOHANG)) > 0) {
if(cpid == dynstage.recompile_pid) {
dynstage.recompile_pid = 0;
}
}
}
void dynstage_init(void) {
struct sigaction sa = {
.sa_sigaction = dynstage_sigchld,
.sa_flags = SA_SIGINFO,
};
if(sigaction(SIGCHLD, &sa, NULL) == -1) {
log_fatal("sigaction() failed: %s", strerror(errno));
}
dynstage_reload_library();
}
static void unwatch_sources(void) {
ht_fwatchset_iter_t iter;
ht_fwatchset_iter_begin(&dynstage.src_watches, &iter);
for(;iter.has_data; ht_fwatchset_iter_next(&iter)) {
filewatch_unwatch(iter.key);
}
ht_fwatchset_iter_end(&iter);
ht_fwatchset_unset_all(&dynstage.src_watches);
}
static void watch_source_file(const char *path) {
FileWatch *w = filewatch_watch(path);
if(!w) {
return;
}
if(is_src_watch(w)) {
// filewatches are reference counted; keep only 1 reference
filewatch_unwatch(w);
} else {
ht_fwatchset_set(&dynstage.src_watches, w, HT_EMPTY);
}
}
static int watch_source_dir(const char *path) {
DIR *d = opendir(path);
if(!d) {
return errno;
}
size_t pathlen = strlen(path);
struct dirent *e;
while((e = readdir(d))) {
const char *n = e->d_name;
if(!*n || !strcmp(n, ".") || !strcmp(n, "..")) {
continue;
}
size_t nlen = strlen(n);
char pathbuf[pathlen + nlen + 2];
memcpy(pathbuf, path, pathlen);
pathbuf[pathlen] = '/';
memcpy(pathbuf + pathlen + 1, n, nlen + 1);
log_debug("%s", pathbuf);
if(watch_source_dir(pathbuf) == ENOTDIR) {
watch_source_file(pathbuf);
}
}
closedir(d);
return 0;
}
static void watch_sources(void) {
watch_source_dir(TAISEI_BUILDCONF_DYNSTAGE_SRCROOT);
}
void dynstage_init_monitoring(void) {
events_register_handler(&(EventHandler) {
.proc = dynstage_frame_event,
.priority = EPRIO_SYSTEM,
.event_type = MAKE_TAISEI_EVENT(TE_FRAME),
});
events_register_handler(&(EventHandler) {
.proc = dynstage_filewatch_event,
.priority = EPRIO_SYSTEM,
.event_type = MAKE_TAISEI_EVENT(TE_FILEWATCH),
});
ht_fwatchset_create(&dynstage.src_watches);
watch_sources();
dynstage.monitoring_initialized = true;
}
static void recompile(void) {
pid_t cpid = fork();
if(UNLIKELY(cpid < 0)) {
log_error("fork() failed: %s", strerror(errno));
return;
}
if(cpid == 0) {
static char *const cmd[] = TAISEI_BUILDCONF_DYNSTAGE_REBUILD_CMD;
execvp(cmd[0], cmd);
abort();
}
log_debug("Child pid: %ji", (intmax_t)cpid);
}
void dynstage_shutdown(void) {
if(dynstage.monitoring_initialized) {
events_unregister_handler(dynstage_frame_event);
events_unregister_handler(dynstage_filewatch_event);
if(dynstage.lib_watch) {
filewatch_unwatch(dynstage.lib_watch);
}
unwatch_sources();
ht_fwatchset_destroy(&dynstage.src_watches);
}
dlclose(dynstage.lib);
sigaction(SIGCHLD, &(struct sigaction) { .sa_handler = SIG_DFL }, NULL);
memset(&dynstage, 0, sizeof(dynstage));
}
uint32_t dynstage_get_generation(void) {
return dynstage.lib_generation;
}
bool dynstage_reload_library(void) {
if(dynstage.lib) {
dlclose(dynstage.lib);
}
dynstage.exports = dlsym((dynstage.lib = dynstage_dlopen()), "stages_exports");
if(UNLIKELY(dynstage.exports == NULL)) {
log_fatal("No stages_exports symbol in stages library");
}
log_info("Reloaded stages module");
return true;
}
StagesExports *dynstage_get_exports(void) {
return dynstage.exports;
}

21
src/dynstage.h Normal file
View file

@ -0,0 +1,21 @@
/*
* 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"
#include "stages/stages.h"
void dynstage_init(void);
void dynstage_init_monitoring(void);
void dynstage_shutdown(void);
bool dynstage_reload_library(void);
StagesExports *dynstage_get_exports(void);
// NOTE: represents the amount of times the on-disk shared object was seen changing.
uint32_t dynstage_get_generation(void);

29
src/dynstage_stub.c Normal file
View file

@ -0,0 +1,29 @@
/*
* 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 "dynstage.h"
#include "stages/stages.h"
void dynstage_init(void) { }
void dynstage_init_monitoring(void) { }
void dynstage_shutdown(void) { }
bool dynstage_reload_library(void) {
return false;
}
StagesExports *dynstage_get_exports(void) {
return &stages_exports;
}
uint32_t dynstage_get_generation(void) {
return 0;
}

View file

@ -31,6 +31,7 @@
#include "cutscenes/cutscene.h"
#include "replay/struct.h"
#include "filewatch/filewatch.h"
#include "dynstage.h"
attr_unused
static void taisei_shutdown(void) {
@ -46,13 +47,13 @@ static void taisei_shutdown(void) {
gamemode_shutdown();
free_all_refs();
shutdown_resources();
filewatch_shutdown();
taskmgr_global_shutdown();
audio_shutdown();
video_shutdown();
gamepad_shutdown();
stageinfo_shutdown();
config_shutdown();
filewatch_shutdown();
vfs_shutdown();
events_shutdown();
time_shutdown();
@ -343,6 +344,7 @@ static void main_post_vfsinit(CallChainResult ccr) {
init_resources();
r_post_init();
draw_loading_screen();
dynstage_init_monitoring();
audio_init();
load_resources();

View file

@ -140,8 +140,6 @@ subdir('stages')
subdir('util')
subdir('vfs')
configure_file(configuration : config, output : 'build_config.h')
taisei_src += [
audio_src,
cutscenes_src,
@ -156,7 +154,6 @@ taisei_src += [
replay_src,
resource_src,
rwops_src,
stages_src,
util_src,
vfs_src,
]
@ -167,6 +164,36 @@ taisei_deps += [
util_deps,
]
if stages_live_reload
taisei_src += files('dynstage.c')
taisei_stages = shared_module('taisei-stages', stages_src, version_deps,
dependencies : taisei_deps,
c_args : taisei_c_args,
c_pch : 'pch/taisei_pch.h',
build_by_default : true,
gnu_symbol_visibility : 'hidden',
)
taisei_deps += cc.find_library('dl', required : false)
taisei_stages_compile_command = [
# FIXME: no good way to get the effective meson command…
'meson', 'compile', '-C', meson.project_build_root(), '-v', taisei_stages.name(),
]
r = run_command(format_array_command, taisei_stages_compile_command, check : true)
config.set_quoted('TAISEI_BUILDCONF_DYNSTAGE_LIB', taisei_stages.full_path())
config.set_quoted('TAISEI_BUILDCONF_DYNSTAGE_SRCROOT', stages_srcdir)
config.set('TAISEI_BUILDCONF_DYNSTAGE_REBUILD_CMD', r.stdout().strip())
else
taisei_src += files('dynstage_stub.c')
taisei_src += stages_src
endif
configure_file(configuration : config, output : 'build_config.h')
taisei_basename = (macos_app_bundle ? 'Taisei' : 'taisei')
if host_machine.system() == 'emscripten'
@ -329,6 +356,7 @@ else
gui_app : not get_option('win_console'),
install : true,
install_dir : bindir,
export_dynamic : stages_live_reload,
)
bindist_deps += taisei
endif

View file

@ -28,13 +28,16 @@
#include "eventloop/eventloop.h"
#include "common_tasks.h"
#include "stageinfo.h"
#include "dynstage.h"
typedef struct StageFrameState {
StageInfo *stage;
CallChain cc;
CoSched sched;
Replay *quicksave;
bool quicksave_is_automatic;
bool quickload_requested;
uint32_t dynstage_generation;
int transition_delay;
int logic_calls;
int desync_check_freq;
@ -362,21 +365,37 @@ static inline bool is_quicksave_allowed(void) {
return true;
}
static void stage_do_quicksave(StageFrameState *fstate, bool isauto) {
if(isauto && fstate->quicksave && !fstate->quicksave_is_automatic) {
// Do not overwrite a manual quicksave with an auto quicksave
return;
}
if(fstate->quicksave) {
replay_reset(fstate->quicksave);
free(fstate->quicksave);
}
fstate->quicksave = create_quicksave_replay(global.replay.output.stage);
fstate->quicksave_is_automatic = isauto;
}
static void stage_do_quickload(StageFrameState *fstate) {
if(fstate->quicksave) {
fstate->quickload_requested = true;
} else {
log_info("No active quicksave");
}
}
static bool stage_input_handler_gameplay(SDL_Event *event, void *arg) {
StageFrameState *fstate = NOT_NULL(arg);
if(event->type == SDL_KEYDOWN && !event->key.repeat && is_quicksave_allowed()) {
if(event->key.keysym.scancode == config_get_int(CONFIG_KEY_QUICKSAVE)) {
if(fstate->quicksave) {
replay_reset(fstate->quicksave);
free(fstate->quicksave);
}
fstate->quicksave = create_quicksave_replay(global.replay.output.stage);
stage_do_quicksave(fstate, false);
} else if(event->key.keysym.scancode == config_get_int(CONFIG_KEY_QUICKLOAD)) {
if(fstate->quicksave) {
fstate->quickload_requested = true;
}
stage_do_quickload(fstate);
}
return false;
@ -462,6 +481,15 @@ static void handle_replay_event(ReplayEvent *e, void *arg) {
}
}
static void leave_replay_mode(StageFrameState *fstate, ReplayState *rp_in) {
if(rp_in->replay == fstate->quicksave) {
audio_sfx_set_enabled(true);
sync_bgm(fstate);
}
replay_state_deinit(rp_in);
}
static void replay_input(StageFrameState *fstate) {
if(!is_quickloading(fstate)) {
events_poll((EventHandler[]){
@ -481,12 +509,7 @@ static void replay_input(StageFrameState *fstate) {
player_applymovement(&global.plr);
if(a.resume_event) {
if(rp_in->replay == fstate->quicksave) {
audio_sfx_set_enabled(true);
sync_bgm(fstate);
}
replay_state_deinit(rp_in);
leave_replay_mode(fstate, rp_in);
}
}
@ -860,6 +883,12 @@ static LogicFrameAction stage_logic_frame(void *arg) {
stage_input(fstate);
}
if(fstate->dynstage_generation != dynstage_get_generation()) {
log_info("Stages library updated, attempting to hot-reload");
stage_do_quicksave(fstate, true); // no-op if user has a manual save
stage_do_quickload(fstate);
}
if(global.gameover != GAMEOVER_TRANSITIONING) {
cosched_run_tasks(&fstate->sched);
@ -891,12 +920,18 @@ static LogicFrameAction stage_logic_frame(void *arg) {
replay_stage_event(global.replay.output.stage, global.frames, EV_CHECK_DESYNC, desync_check);
}
if(
rpsync == REPLAY_SYNC_FAIL &&
global.is_replay_verification &&
!global.replay.output.stage
) {
exit(1);
if(rpsync == REPLAY_SYNC_FAIL) {
if(
global.is_replay_verification &&
!global.replay.output.stage
) {
exit(1);
}
if(fstate->quicksave && fstate->quicksave == global.replay.input.replay) {
log_warn("Quicksave replay desynced; resuming prematurely!");
leave_replay_mode(fstate, &global.replay.input);
}
}
stage_logic();
@ -962,7 +997,12 @@ static void stage_end_loop(void *ctx);
static void stage_stub_proc(void) { }
static void _stage_enter(StageInfo *stage, CallChain next, Replay *quickload) {
static void _stage_enter(
StageInfo *stage, CallChain next, Replay *quickload, bool quicksave_is_automatic
) {
uint32_t dynstage_generation = dynstage_get_generation();
stageinfo_reload();
assert(stage);
assert(stage->procs);
@ -1064,7 +1104,9 @@ static void _stage_enter(StageInfo *stage, CallChain next, Replay *quickload) {
fstate->stage = stage;
fstate->cc = next;
fstate->quicksave = quickload;
fstate->quicksave_is_automatic = quicksave_is_automatic;
fstate->desync_check_freq = env_get("TAISEI_REPLAY_DESYNC_CHECK_FREQUENCY", FPS * 5);
fstate->dynstage_generation = dynstage_generation;
_current_stage_state = fstate;
@ -1085,7 +1127,7 @@ static void _stage_enter(StageInfo *stage, CallChain next, Replay *quickload) {
}
void stage_enter(StageInfo *stage, CallChain next) {
_stage_enter(stage, next, NULL);
_stage_enter(stage, next, NULL, false);
}
void stage_end_loop(void *ctx) {
@ -1093,6 +1135,7 @@ void stage_end_loop(void *ctx) {
assert(s == _current_stage_state);
Replay *quicksave = s->quicksave;
bool quicksave_is_automatic = s->quicksave_is_automatic;
bool is_quickload = s->quickload_requested;
if(is_quickload) {
@ -1145,7 +1188,7 @@ void stage_end_loop(void *ctx) {
free(s);
if(is_quickload) {
_stage_enter(stginfo, cc, quicksave);
_stage_enter(stginfo, cc, quicksave, quicksave_is_automatic);
} else {
run_call_chain(&cc, NULL);
}

View file

@ -9,25 +9,13 @@
#include "taisei.h"
#include "stageinfo.h"
#include "stages/stages.h"
#include "stages/stage1/stage1.h"
#include "stages/stage2/stage2.h"
#include "stages/stage3/stage3.h"
#include "stages/stage4/stage4.h"
#include "stages/stage5/stage5.h"
#include "stages/stage6/stage6.h"
#include "stages/extra.h"
#ifdef DEBUG
#define DPSTEST
#include "stages/dpstest.h"
#define COROTEST
#include "stages/corotest.h"
#endif
#include "dynstage.h"
static struct {
DYNAMIC_ARRAY(StageInfo) stages;
StageProgress **stages_progress;
} stageinfo;
static void add_stage(
@ -104,37 +92,71 @@ static bool spellfilter_extra(AttackInfo *spell) {
return spell->type == AT_ExtraSpell;
}
static void stageinfo_fill(StagesExports *e);
void stageinfo_init(void) {
dynstage_init();
stageinfo_fill(dynstage_get_exports());
stageinfo.stages_progress = calloc(stageinfo.stages.num_elements, sizeof(*stageinfo.stages_progress));
}
void stageinfo_reload(void) {
if(!dynstage_reload_library()) {
log_debug("dynstage not reloaded");
return;
}
attr_unused StageInfo *prev_addr = stageinfo.stages.data;
attr_unused uint prev_count = stageinfo.stages.num_elements;
dynarray_foreach_elem(&stageinfo.stages, StageInfo *stg, {
free(stg->title);
free(stg->subtitle);
});
stageinfo.stages.num_elements = 0;
stageinfo_fill(dynstage_get_exports());
assert(stageinfo.stages.data == prev_addr);
assert(stageinfo.stages.num_elements == prev_count);
log_debug("Stageinfo updated after dynstage reload");
}
static void stageinfo_fill(StagesExports *e) {
int spellnum = 0;
// id procs type title subtitle spells diff
add_stage(1, &stage1_procs, STAGE_STORY, "Stage 1", "Misty Lake", (AttackInfo*)&stage1_spells, D_Any);
add_stage(2, &stage2_procs, STAGE_STORY, "Stage 2", "Walk Along the Border", (AttackInfo*)&stage2_spells, D_Any);
add_stage(3, &stage3_procs, STAGE_STORY, "Stage 3", "Through the Tunnel of Light", (AttackInfo*)&stage3_spells, D_Any);
add_stage(4, &stage4_procs, STAGE_STORY, "Stage 4", "Forgotten Mansion", (AttackInfo*)&stage4_spells, D_Any);
add_stage(5, &stage5_procs, STAGE_STORY, "Stage 5", "Climbing the Tower of Babel", (AttackInfo*)&stage5_spells, D_Any);
add_stage(6, &stage6_procs, STAGE_STORY, "Stage 6", "Roof of the World", (AttackInfo*)&stage6_spells, D_Any);
// id procs type title subtitle spells diff
add_stage(1, e->stage1.procs, STAGE_STORY, "Stage 1", "Misty Lake", e->stage1.spells, D_Any);
add_stage(2, e->stage2.procs, STAGE_STORY, "Stage 2", "Walk Along the Border", e->stage2.spells, D_Any);
add_stage(3, e->stage3.procs, STAGE_STORY, "Stage 3", "Through the Tunnel of Light", e->stage3.spells, D_Any);
add_stage(4, e->stage4.procs, STAGE_STORY, "Stage 4", "Forgotten Mansion", e->stage4.spells, D_Any);
add_stage(5, e->stage5.procs, STAGE_STORY, "Stage 5", "Climbing the Tower of Babel", e->stage5.spells, D_Any);
add_stage(6, e->stage6.procs, STAGE_STORY, "Stage 6", "Roof of the World", e->stage6.spells, D_Any);
add_stage(7, e->stagex.procs, STAGE_SPECIAL, "Extra Stage", "Descent into Madness", e->stagex.spells, D_Extra);
#ifdef DPSTEST
add_stage(0x40|0, &stage_dpstest_single_procs, STAGE_SPECIAL, "DPS Test", "Single target", NULL, D_Normal);
add_stage(0x40|1, &stage_dpstest_multi_procs, STAGE_SPECIAL, "DPS Test", "Multiple targets", NULL, D_Normal);
add_stage(0x40|2, &stage_dpstest_boss_procs, STAGE_SPECIAL, "DPS Test", "Boss", NULL, D_Normal);
#ifdef TAISEI_BUILDCONF_TESTING_STAGES
add_stage(0x40|0, e->testing.dps_single, STAGE_SPECIAL, "DPS Test", "Single target", NULL, D_Normal);
add_stage(0x40|1, e->testing.dps_multi, STAGE_SPECIAL, "DPS Test", "Multiple targets", NULL, D_Normal);
add_stage(0x40|2, e->testing.dps_boss, STAGE_SPECIAL, "DPS Test", "Boss", NULL, D_Normal);
#endif
// generate spellpractice stages
add_spellpractice_stages(&spellnum, spellfilter_normal, STAGE_SPELL_BIT);
add_spellpractice_stages(&spellnum, spellfilter_extra, STAGE_SPELL_BIT | STAGE_EXTRASPELL_BIT);
#ifdef SPELL_BENCHMARK
add_spellpractice_stage(dynarray_get_ptr(&stageinfo.stages, 0), &stage1_spell_benchmark, &spellnum, STAGE_SPELL_BIT, D_Extra);
#endif
#ifdef TAISEI_BUILDCONF_TESTING_STAGES
add_spellpractice_stage(
dynarray_get_ptr(&stageinfo.stages, 0),
e->testing.benchmark_spell, &spellnum, STAGE_SPELL_BIT, D_Extra
);
#ifdef COROTEST
add_stage(0xC0, &corotest_procs, STAGE_SPECIAL, "Coroutines!", "wow such concurrency very async", NULL, D_Any);
add_stage(
0xC0, e->testing.coro, STAGE_SPECIAL,
"Coroutines!", "wow such concurrency very async", NULL, D_Any
);
#endif
add_stage(0xC1, &extra_procs, STAGE_SPECIAL, "Extra Stage", "Descent into Madness", NULL, D_Extra);
dynarray_compact(&stageinfo.stages);
#ifdef DEBUG
@ -153,13 +175,15 @@ void stageinfo_init(void) {
}
void stageinfo_shutdown(void) {
dynarray_foreach_elem(&stageinfo.stages, StageInfo *stg, {
dynarray_foreach(&stageinfo.stages, int i, StageInfo *stg, {
free(stg->title);
free(stg->subtitle);
free(stg->progress);
free(stageinfo.stages_progress[i]);
});
dynarray_free_data(&stageinfo.stages);
free(stageinfo.stages_progress);
dynstage_shutdown();
}
size_t stageinfo_get_num_stages(void) {
@ -203,23 +227,23 @@ StageProgress *stageinfo_get_progress(StageInfo *stage, Difficulty diff, bool al
// This stuff must stay around until progress_save(), which happens on shutdown.
// So do NOT try to free any pointers this function returns, that will fuck everything up.
uint idx = dynarray_indexof(&stageinfo.stages, stage);
StageProgress **prog = stageinfo.stages_progress + idx;
bool fixed_diff = (stage->difficulty != D_Any);
if(!fixed_diff && (diff < D_Easy || diff > D_Lunatic)) {
return NULL;
}
if(!stage->progress) {
if(!*prog) {
if(!allocate) {
return NULL;
}
size_t allocsize = sizeof(StageProgress) * (fixed_diff ? 1 : NUM_SELECTABLE_DIFFICULTIES);
stage->progress = malloc(allocsize);
memset(stage->progress, 0, allocsize);
*prog = calloc(fixed_diff ? 1 : NUM_SELECTABLE_DIFFICULTIES, sizeof(**prog));
}
return stage->progress + (fixed_diff ? 0 : diff - D_Easy);
return *prog + (fixed_diff ? 0 : diff - D_Easy);
}
StageProgress *stageinfo_get_progress_by_id(uint16_t id, Difficulty diff, bool allocate) {

View file

@ -49,10 +49,6 @@ typedef struct StageInfo {
char *subtitle;
AttackInfo *spell;
Difficulty difficulty;
// Do NOT access this directly!
// Use stage_get_progress or stage_get_progress_from_info, which will lazy-initialize it and pick the correct offset.
StageProgress *progress;
} StageInfo;
size_t stageinfo_get_num_stages(void);
@ -66,3 +62,4 @@ StageProgress *stageinfo_get_progress_by_id(uint16_t id, Difficulty diff, bool a
void stageinfo_init(void);
void stageinfo_shutdown(void);
void stageinfo_reload(void);

View file

@ -1,5 +1,7 @@
stages_src = []
stages_src = files(
'stages.c',
)
stages = [
'stage1',
@ -22,9 +24,11 @@ stages_src += files(
'extra.c',
)
if is_developer_build
if use_testing_stages
stages_src += files(
'dpstest.c',
'corotest.c',
)
endif
stages_srcdir = meson.current_source_dir()

View file

@ -38,7 +38,6 @@ extern struct stage1_spells_s {
} stage1_spells;
extern StageProcs stage1_procs;
extern StageProcs stage1_spell_procs;
#ifdef SPELL_BENCHMARK
extern AttackInfo stage1_spell_benchmark;

43
src/stages/stages.c Normal file
View file

@ -0,0 +1,43 @@
/*
* 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 "stages.h"
#include "stages/stage1/stage1.h"
#include "stages/stage2/stage2.h"
#include "stages/stage3/stage3.h"
#include "stages/stage4/stage4.h"
#include "stages/stage5/stage5.h"
#include "stages/stage6/stage6.h"
#include "stages/extra.h"
#ifdef TAISEI_BUILDCONF_TESTING_STAGES
#include "stages/dpstest.h"
#include "stages/corotest.h"
#endif
StagesExports stages_exports = {
.stage1 = { &stage1_procs, (AttackInfo*)&stage1_spells },
.stage2 = { &stage2_procs, (AttackInfo*)&stage2_spells },
.stage3 = { &stage3_procs, (AttackInfo*)&stage3_spells },
.stage4 = { &stage4_procs, (AttackInfo*)&stage4_spells },
.stage5 = { &stage5_procs, (AttackInfo*)&stage5_spells },
.stage6 = { &stage6_procs, (AttackInfo*)&stage6_spells },
.stagex = { &extra_procs, NULL },
#ifdef TAISEI_BUILDCONF_TESTING_STAGES
.testing = {
.dps_single = &stage_dpstest_single_procs,
.dps_multi = &stage_dpstest_multi_procs,
.dps_boss = &stage_dpstest_boss_procs,
.coro = &corotest_procs,
.benchmark_spell = &stage1_spell_benchmark,
}
#endif
};

31
src/stages/stages.h Normal file
View file

@ -0,0 +1,31 @@
/*
* 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"
#include "stage.h"
typedef struct StagesExports {
struct {
StageProcs *procs;
AttackInfo *spells;
} stage1, stage2, stage3, stage4, stage5, stage6, stagex;
#ifdef TAISEI_BUILDCONF_TESTING_STAGES
struct {
StageProcs *dps_single, *dps_multi, *dps_boss, *coro;
AttackInfo *benchmark_spell;
} testing;
#endif
} StagesExports;
#ifdef TAISEI_BUILDCONF_DYNSTAGE
__attribute__((visibility("default")))
#endif
extern StagesExports stages_exports;