stageinfo,stage: implement stage hot-reloading
POSIX platforms only. Must be enabled via -Dstages_live_reload=true.
This commit is contained in:
parent
1260dcc0c3
commit
dcb630b37d
17 changed files with 652 additions and 77 deletions
15
meson.build
15
meson.build
|
@ -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)
|
||||
|
|
|
@ -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
8
scripts/format-array.py
Executable 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']), "}")
|
|
@ -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]
|
||||
|
|
10
src/boss.c
10
src/boss.c
|
@ -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
315
src/dynstage.c
Normal 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
21
src/dynstage.h
Normal 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
29
src/dynstage_stub.c
Normal 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;
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
91
src/stage.c
91
src/stage.c
|
@ -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);
|
||||
}
|
||||
|
|
104
src/stageinfo.c
104
src/stageinfo.c
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
43
src/stages/stages.c
Normal 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
31
src/stages/stages.h
Normal 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;
|
Loading…
Reference in a new issue