Emscripten compatibility (#161)

* Major refactoring of the main loop(s) and control flow (WIP)

run_at_fps() is gone 🦀

Instead of nested blocking event loops, there is now an eventloop API
that manages an explicit stack of scenes. This makes Taisei a lot more
portable to async environments where spinning a loop forever without
yielding control simply is not an option, and that is the entire point
of this change.

A prime example of such an environment is the Web (via emscripten).
Taisei was able to run there through a terrible hack: inserting
emscripten_sleep calls into the loop, which would yield to the browser.
This has several major drawbacks: first of all, every function that
could possibly call emscripten_sleep must be compiled into a special
kind of bytecode, which then has to be interpreted at runtime, *much*
slower than JITed WebAssembly. And that includes *everything* down the
call stack, too! For more information, see
https://emscripten.org/docs/porting/emterpreter.html

Even though that method worked well enough for experimenting, despite
suboptimal performance, there is another obvious drawback:
emscripten_sleep is implemented via setTimeout(), which can be very
imprecise and is generally not reliable for fluid animation. Browsers
actually have an API specifically for that use case:
window.requestAnimationFrame(), but Taisei's original blocking control
flow style is simply not compatible with it. Emscripten exposes this API
with its emscripten_set_main_loop(), which the eventloop backend now
uses on that platform.

Unfortunately, C is still C, with no fancy closures or coroutines.
With blocking calls into menu/scene loops gone, the control flow is
reimplemented via so-called (pun intended) "call chains". That is
basically an euphemism for callback hell. With manual memory management
and zero type-safety. Not that the menu system wasn't shitty enough
already. I'll just keep telling myself that this is all temporary and
will be replaced with scripts in v1.4.

* improve build system for emscripten + various fixes

* squish menu bugs

* improve emscripten event loop; disable EMULATE_FUNCTION_POINTER_CASTS

Note that stock freetype does not work without
EMULATE_FUNCTION_POINTER_CASTS; use a patched version from the
"emscripten" branch here:

    https://github.com/taisei-project/freetype2/tree/emscripten

* Enable -Wcast-function-type

Calling functions through incompatible pointers is nasal demons and
doesn't work in WASM.

* webgl: workaround a crash on some browsers

* emscripten improvements:

    * Persist state (config, progress, replays, ...) in local IndexDB
    * Simpler HTML shell (temporary)
    * Enable more optimizations

* fix build if validate_glsl=false

* emscripten: improve asset packaging, with local cache

Note that even though there are rules to build audio bundles, audio
does *not* work yet. It looks like SDL2_mixer can not work without
threads, which is a problem. Yet another reason to write an OpenAL
backend - emscripten supports that natively.

* emscripten: customize the html shell

* emscripten: force "show log" checkbox unchecked initially

* emscripten: remove quit shortcut from main menu (since there's no quit)

* emscripten: log area fixes

* emscripten/webgl: workaround for fullscreen viewport issue

* emscripten: implement frameskip

* emscripter: improve framerate limiter

* align List to at least 8 bytes (shut up warnings)

* fix non-emscripten builds

* improve fullscreen handling, mainly for emscripten

* Workaround to make audio work in chromium

emscripten-core/emscripten#6511

* emscripten: better vsync handling; enable vsync & disable fxaa by default
This commit is contained in:
Andrei Alexeyev 2019-03-09 21:32:32 +02:00 committed by GitHub
parent c8e281bc02
commit 180f9e3856
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
94 changed files with 2505 additions and 830 deletions

View file

@ -34,7 +34,7 @@ Build-only dependencies
^^^^^^^^^^^^^^^^^^^^^^^
- Python >= 3.5
- meson >= 0.45.0 (build system; >=0.48.0 recommended)
- meson >= 0.46.0 (build system; >=0.48.0 recommended)
Optional:

BIN
emscripten/background.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

BIN
emscripten/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

14
emscripten/meson.build Normal file
View file

@ -0,0 +1,14 @@
em_preamble = files('preamble.js')
em_shell = files('shell.html')
if host_machine.system() == 'emscripten'
install_data(
files(
'background.webp',
'scythe.webp',
'favicon.ico',
),
install_dir : bindir
)
endif

20
emscripten/preamble.js Normal file
View file

@ -0,0 +1,20 @@
Module['preRun'].push(function() {
ENV["TAISEI_NOASYNC"] = "1";
ENV["TAISEI_NOUNLOAD"] = "1";
ENV["TAISEI_PREFER_SDL_VIDEODRIVERS"] = "emscripten";
ENV["TAISEI_RENDERER"] = "gles30";
FS.mkdir('/persistent');
FS.mount(IDBFS, {}, '/persistent');
});
function SyncFS(is_load, ccptr) {
FS.syncfs(is_load, function(err) {
Module['ccall'](
'vfs_sync_callback',
null, ["boolean", "string", "number"],
[is_load, err, ccptr]
);
});
};

BIN
emscripten/scythe.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

269
emscripten/shell.html Normal file
View file

@ -0,0 +1,269 @@
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<link rel="icon" type="image/x-icon" href="favicon.ico"/>
<title>Taisei Project — Web version (Experimental!)</title>
<style>
body {
background-image: url('background.webp');
background-color: #111111;
background-repeat: no-repeat;
background-position: 50% 50%;
background-size: cover;
color: #eeeeee;
height: 100%;
margin: 0px;
padding: 0px;
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
}
html {
background-color: #000000;
height: 100%;
margin: 0px;
padding: 0px;
}
.emscripten {
padding-right: 0;
margin-left: auto;
margin-right: auto;
display: block;
}
textarea.emscripten {
font-family: monospace;
width: 100%;
resize: none;
background: none;
border: 0px none;
color: #eeeeee;
overflow: overlay;
margin: 0px;
}
div.emscripten {
text-align: center;
}
div.emscripten_border {
border: 0px none;
}
div.centered {
position: absolute;
bottom: 50%;
left: 0;
right: 0;
transform: translateY(50%);
background-color: #000000C0;
padding: 16px 0px;
}
/* the canvas *must not* have any border or padding, or mouse coords will be wrong */
canvas.emscripten {
border: 0px none;
background-color: none;
}
#spinner {
overflow: visible;
padding-bottom: 16px;
}
#progress {
margin: 4px;
}
#logContainer {
width: 80%;
margin: auto;
}
#logToggleContainer {
display: /* inline-block */ none;
position: relative;
left: 50%;
transform: translateX(-50%);
opacity: 0.5;
}
progress {
-webkit-appearance: none;
-moz-appearance: none;
border: 0px none;
background-color: #eee;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25) inset;
}
progress::-webkit-progress-bar {
background-color: #eee;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25) inset;
}
progress::-webkit-progress-value {
background-image:
-webkit-linear-gradient(top, rgba(0, 0, 0, 0.25), rgba(255, 255, 255, 0.25)),
-webkit-linear-gradient(left, #5c7da8, #6db0ed);
border-radius: 5px;
background-size: 35px 20px, 100% 100%, 100% 100%;
}
progress::-moz-progress-bar {
background-image:
-moz-linear-gradient(top, rgba(0, 0, 0, 0.25), rgba(255, 255, 255, 0.25)),
-moz-linear-gradient(left, #5c7da8, #6db0ed);
border-radius: 5px;
background-size: 35px 20px, 100% 100%, 100% 100%;
}
div.spinner {
height: 75px;
width: 75px;
margin: 0px auto;
}
img.spinner {
height: 75px;
width: 75px;
margin: 4px auto;
-webkit-animation: rotation 0.8s linear infinite;
-moz-animation: rotation 0.8s linear infinite;
-o-animation: rotation 0.8s linear infinite;
animation: rotation 0.6s linear infinite;
border: 0px none;
}
@-webkit-keyframes rotation {
from { -webkit-transform: rotate(0deg); }
to { -webkit-transform: rotate(360deg); }
}
@-moz-keyframes rotation {
from { -moz-transform: rotate(0deg); }
to { -moz-transform: rotate(360deg); }
}
@-o-keyframes rotation {
from { -o-transform: rotate(0deg); }
to { -o-transform: rotate(360deg); }
}
@keyframes rotation {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="centered">
<div class="spinner" id="spinner"><img src="scythe.webp" class="spinner"/></div>
<div class="emscripten" id="status">Girls are now downloading, please wait warmly…</div>
<div class="emscripten">
<progress value="0" max="100" id="progress" ></progress>
</div>
<div class="emscripten_border" id="canvasContainer" hidden>
<canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1></canvas>
</div>
<div id="logToggleContainer" hidden>
<input type="checkbox" name="logToggle" id="logToggle" onclick="toggleLog()"/>
<label for="logToggle"> Show log</label>
</div>
<div id="logContainer" hidden>
<textarea class="emscripten" id="output" rows="12" readonly></textarea>
</div>
</div>
<script>
var statusElement = document.getElementById('status');
var progressElement = document.getElementById('progress');
var spinnerElement = document.getElementById('spinner');
var canvasContainerElement = document.getElementById('canvasContainer');
var logToggleElement = document.getElementById('logToggle');
var logContainerElement = document.getElementById('logContainer');
var logOutputElement = document.getElementById('output');
var dlMessage = statusElement.innerText;
logToggleElement.checked = false;
function toggleLog() {
logContainerElement.hidden = !logToggleElement.checked;
logOutputElement.scrollTop = logOutputElement.scrollHeight;
}
var Taisei = {
preRun: [],
postRun: [],
onFirstFrame: function() {
canvasContainerElement.hidden = false;
logToggleContainer.style.display = "inline-block";
Taisei.setStatus('', true);
},
print: (function() {
logOutputElement.value = ''; // clear browser cache
return function(text) {
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
console.log(text);
logOutputElement.value += text + "\n";
logOutputElement.scrollTop = logOutputElement.scrollHeight; // focus on bottom
};
})(),
printErr: function(text) {
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
console.error(text);
},
canvas: (function() {
var canvas = document.getElementById('canvas');
// As a default initial behavior, pop up an alert when webgl context is lost. To make your
// application robust, you may want to override this behavior before shipping!
// See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
canvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false);
return canvas;
})(),
setStatus: function(text, force) {
if (!text && !force) return;
if (!Taisei.setStatus.last) Taisei.setStatus.last = { time: Date.now(), text: '' };
if (text === Taisei.setStatus.last.text) return;
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
var now = Date.now();
if (m && now - Taisei.setStatus.last.time < 30) return; // if this is a progress update, skip it if too soon
Taisei.setStatus.last.time = now;
Taisei.setStatus.last.text = text;
if (m) {
text = m[1];
progressElement.value = parseInt(m[2])*100;
progressElement.max = parseInt(m[4])*100;
progressElement.hidden = false;
spinnerElement.hidden = false;
} else {
progressElement.value = null;
progressElement.max = null;
progressElement.hidden = true;
if (!text) spinnerElement.hidden = true;
}
statusElement.innerText = text.replace(/^Downloading(?: data)?\.\.\./, dlMessage).replace('...', '…');
console.log("[STATUS] " + statusElement.innerText);
},
totalDependencies: 0,
monitorRunDependencies: function(left) {
this.totalDependencies = Math.max(this.totalDependencies, left);
Taisei.setStatus(left ? 'Preparing… (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
}
};
window.onerror = function(error) {
Taisei.setStatus('Error: ' + error);
};
</script>
{{{ SCRIPT }}}
</body>
</html>

14
external/meson.build vendored
View file

@ -1,5 +1,11 @@
install_data(
join_paths('gamecontrollerdb', 'gamecontrollerdb.txt'),
install_dir : data_path
)
gamecontrollerdb_name = 'gamecontrollerdb.txt'
gamecontrollerdb_relpath = join_paths('gamecontrollerdb', gamecontrollerdb_name)
gamecontrollerdb_path = join_paths(meson.current_source_dir(), gamecontrollerdb_relpath)
if host_machine.system() != 'emscripten'
install_data(
gamecontrollerdb_relpath,
install_dir : data_path
)
endif

View file

@ -1,7 +1,7 @@
project('taisei', 'c',
license : 'MIT',
version : 'v1.3-dev',
meson_version : '>=0.45.0',
meson_version : '>=0.46.0',
default_options : [
'c_std=c11',
@ -34,6 +34,7 @@ taisei_c_args = [
'-Wabsolute-value',
'-Wcast-align',
'-Wcast-align=strict',
'-Wcast-function-type',
'-Wclobbered',
'-Wduplicated-branches',
'-Wduplicated-cond',
@ -90,7 +91,7 @@ if sm_check.stderr() != ''
warning('Submodule check completed with errors:\n@0@'.format(sm_check.stderr()))
endif
static = get_option('static')
static = get_option('static') or (host_machine.system() == 'emscripten')
dep_freetype = dependency('freetype2', required : true, static : static)
dep_png = dependency('libpng', version : '>=1.5', required : true, static : static)
@ -130,9 +131,14 @@ if host_machine.system() == 'windows'
taisei_deps += cc.find_library('shlwapi')
endif
package_data = get_option('package_data')
enable_zip = get_option('enable_zip')
package_data = (package_data == 'auto' ? enable_zip : package_data == 'true')
if host_machine.system() == 'emscripten'
package_data = false
enable_zip = false
else
package_data = get_option('package_data')
enable_zip = get_option('enable_zip')
package_data = (package_data == 'auto' ? enable_zip : package_data == 'true')
endif
if enable_zip
if not dep_zip.found()
@ -163,6 +169,9 @@ config.set('TAISEI_BUILDCONF_HAVE_MAX_ALIGN_T', cc.compiles('#include <stddef.h>
prefer_relpath_systems = [
'windows',
]
force_relpath_systems = [
'emscripten',
]
@ -183,7 +192,9 @@ if macos_app_bundle
else
datadir = get_option('datadir')
if get_option('install_relative') == 'auto'
if force_relpath_systems.contains(host_machine.system())
config.set('TAISEI_BUILDCONF_RELATIVE_DATA_PATH', true)
elif get_option('install_relative') == 'auto'
config.set('TAISEI_BUILDCONF_RELATIVE_DATA_PATH', prefer_relpath_systems.contains(host_machine.system()))
else
config.set('TAISEI_BUILDCONF_RELATIVE_DATA_PATH', get_option('install_relative') == 'true')
@ -275,6 +286,7 @@ endif
version_deps = []
subdir('misc')
subdir('emscripten')
subdir('external')
subdir('resources')
subdir('doc')

View file

@ -37,7 +37,7 @@ if validate_glsl
fname = '@0@'.format(src)
stage = fname.split('.')[-2]
glsl_targets += custom_target(fname.underscorify(),
spirv = custom_target('SPIRV_' + fname.underscorify(),
input : src,
output : '@BASENAME@.spv',
command : [
@ -48,6 +48,26 @@ if validate_glsl
build_by_default : true,
depfile : '@0@.d'.format(fname.underscorify()),
)
spirv_targets += spirv
if transpile_glsl
essl = custom_target('ESSL_' + fname.underscorify(),
input : spirv,
output : '@BASENAME@.glsl',
command : [
spvc_command,
'--output', '@OUTPUT@', '@INPUT@',
spvc_args,
get_variable('spvc_@0@_args'.format(stage)),
],
install : false,
build_by_default : true,
depfile : '@0@.d'.format(fname.underscorify()),
)
essl_targets += essl
endif
endforeach
endif
# @end validate

View file

@ -91,9 +91,8 @@ subdirs = [
# @end subdirs
]
glsl_targets = []
validate_glsl = false
spirv_targets = []
essl_targets = []
glslc_args = [
# '-fauto-bind-uniforms',
@ -124,9 +123,31 @@ glslc_vert_args = [
'-DVERT_STAGE',
]
if get_option('validate_glsl') != 'false'
spvc_args = [
'--version', '300',
'--es',
'--remove-unused-variables',
]
spvc_frag_args = [
'--stage', 'frag',
]
spvc_vert_args = [
'--stage', 'vert',
]
if host_machine.system() == 'emscripten'
validate_glsl = 'true'
transpile_glsl = true
else
validate_glsl = get_option('validate_glsl')
transpile_glsl = false
endif
if validate_glsl != 'false'
glslc_command = find_program('glslc',
required : (get_option('validate_glsl') == 'true')
required : (validate_glsl == 'true')
)
if glslc_command.found()
@ -143,13 +164,23 @@ if get_option('validate_glsl') != 'false'
else
warning(test_result.stderr())
if get_option('validate_glsl') == 'auto'
if validate_glsl == 'auto'
warning('glslc test failed, you probably have an incompatible version. GLSL validation will be disabled.')
validate_glsl = false
else
error('glslc test failed, you probably have an incompatible version.')
endif
endif
else
validate_glsl = false
endif
if validate_glsl and transpile_glsl
spvc_command = find_program('spirv-cross', required : true)
glslc_args += ['-Os', '-g']
endif
else
validate_glsl = false
endif
# @begin validate
@ -158,7 +189,7 @@ if validate_glsl
fname = '@0@'.format(src)
stage = fname.split('.')[-2]
glsl_targets += custom_target(fname.underscorify(),
spirv = custom_target('SPIRV_' + fname.underscorify(),
input : src,
output : '@BASENAME@.spv',
command : [
@ -169,6 +200,26 @@ if validate_glsl
build_by_default : true,
depfile : '@0@.d'.format(fname.underscorify()),
)
spirv_targets += spirv
if transpile_glsl
essl = custom_target('ESSL_' + fname.underscorify(),
input : spirv,
output : '@BASENAME@.glsl',
command : [
spvc_command,
'--output', '@OUTPUT@', '@INPUT@',
spvc_args,
get_variable('spvc_@0@_args'.format(stage)),
],
install : false,
build_by_default : true,
depfile : '@0@.d'.format(fname.underscorify()),
)
essl_targets += essl
endif
endforeach
endif
# @end validate
@ -176,3 +227,5 @@ endif
foreach sd : subdirs
subdir(sd)
endforeach
shaders_build_dir = meson.current_build_dir()

View file

@ -5,6 +5,50 @@ packages = [
'00-taisei',
]
if host_machine.system() == 'emscripten'
em_data_prefix = '/@0@'.format(config.get_unquoted('TAISEI_BUILDCONF_DATA_PATH'))
em_bundles = ['gfx', 'misc']
if get_option('a_default') != 'null'
em_bundles += ['bgm', 'sfx']
endif
em_bundle_gfx_patterns = [
'gfx/*.png',
'gfx/*.webp',
'fonts/*.ttf',
'fonts/*.otf',
]
em_bundle_misc_patterns = [
'gfx/*.ani',
'gfx/*.spr',
'gfx/*.tex',
'fonts/*.font',
'models/*.obj',
# We don't want to include the shader sources here, we're going to translate them first.
'shader/*.prog'
]
em_bundle_bgm_patterns = [
'bgm/*',
]
em_bundle_sfx_patterns = [
'sfx/*',
]
foreach bundle : em_bundles
set_variable('em_bundle_@0@_deps'.format(bundle), [])
set_variable('em_bundle_@0@_files'.format(bundle), [])
set_variable('em_bundle_@0@_packer_args'.format(bundle), [])
endforeach
# These are all text files that compress well
em_bundle_misc_packer_args += ['--lz4']
endif
foreach pkg : packages
pkg_pkgdir = '@0@.pkgdir'.format(pkg)
pkg_zip = '@0@.zip'.format(pkg)
@ -12,7 +56,26 @@ foreach pkg : packages
subdir(pkg_pkgdir)
if package_data
if host_machine.system() == 'emscripten'
foreach bundle : em_bundles
var_patterns = 'em_bundle_@0@_patterns'.format(bundle)
var_files = 'em_bundle_@0@_files'.format(bundle)
var_packer_args = 'em_bundle_@0@_packer_args'.format(bundle)
glob_result = run_command(glob_command, pkg_path, get_variable(var_patterns))
assert(glob_result.returncode() == 0, 'Glob script failed')
foreach file : glob_result.stdout().strip().split('\n')
fpath = join_paths(meson.current_source_dir(), pkg_pkgdir, file)
set_variable(var_files, get_variable(var_files) + [fpath])
set_variable(var_packer_args, get_variable(var_packer_args) + [
'--preload', '@0@@@1@/@2@'.format(fpath, em_data_prefix, file)
])
endforeach
endforeach
elif package_data
custom_target(pkg_zip,
command : [pack_command,
pkg_path,
@ -30,3 +93,69 @@ foreach pkg : packages
install_subdir(pkg_pkgdir, install_dir : data_path, exclude_files : glob_result.stdout().split('\n'))
endif
endforeach
if host_machine.system() == 'emscripten'
# First add some stuff that isn't sourced from resources/
em_bundle_misc_files += gamecontrollerdb_path
em_bundle_misc_packer_args += [
'--preload', '@0@@@1@/@2@'.format(gamecontrollerdb_path, em_data_prefix, gamecontrollerdb_name)
]
foreach shader : essl_targets
# This one is especially dirty!
abs_path = shader.full_path()
assert(abs_path.startswith(shaders_build_dir), 'Assumption about shader output location violated')
rel_path = '@0@/shader@1@'.format(em_data_prefix, abs_path.split(shaders_build_dir)[1])
em_bundle_misc_deps += shader
em_bundle_misc_packer_args += [
'--preload', '@0@@@1@'.format(abs_path, rel_path)
]
endforeach
packer = find_program('file_packager') # should be provided by cross file
em_bundle_link_args = [] # We'll pass these to the final emcc linking step
# Finally, set up build targets for our bundles
foreach bundle : em_bundles
var_files = 'em_bundle_@0@_files'.format(bundle)
var_deps = 'em_bundle_@0@_deps'.format(bundle)
var_packer_args = 'em_bundle_@0@_packer_args'.format(bundle)
data_name = 'bundle_@0@.data'.format(bundle)
loader_name = 'bundle_@0@.js'.format(bundle)
out_loader = join_paths(meson.current_build_dir(), '@0@.raw'.format(loader_name))
bundle_data = custom_target(data_name,
command : [
packer,
'@OUTPUT0@',
'--js-output=@0@'.format(out_loader), # No, this one does not accept "--js-output foobar.js"
'--use-preload-cache',
'--no-heap-copy',
'--from-emcc',
get_variable(var_packer_args)
],
output : data_name,
depends : get_variable(var_deps),
depend_files : get_variable(var_files),
install : true,
install_dir : bindir,
)
bundle_loader = custom_target(loader_name,
command : [
em_set_bundle_uuid_command,
out_loader,
'--sha1', bundle_data,
'--output', '@OUTPUT@',
],
output : loader_name,
install : false,
)
em_bundle_link_args += ['--pre-js', bundle_loader]
endforeach
endif

66
scripts/em-set-bundle-uuid.py Executable file
View file

@ -0,0 +1,66 @@
#!/usr/bin/env python3
from taiseilib.common import (
run_main,
update_text_file,
)
import argparse
import hashlib
import json
import re
from pathlib import Path
meta_re = re.compile(r'(.*loadPackage\()({.*?})(\);.*)', re.DOTALL)
def main(args):
parser = argparse.ArgumentParser(description='Change package UUID in JavaScript loader generated by Emscripten\'s file_packager.py', prog=args[0])
parser.add_argument('loader',
help='the .js loader file',
metavar='FILE',
type=Path,
)
parser.add_argument('--output', '-o',
help='write result to FILE (default: overwrite input)',
metavar='FILE',
type=Path,
)
g = parser.add_mutually_exclusive_group(required=True)
g.add_argument('--uuid',
help='manually specify an UUID',
metavar='UUID',
type=str,
)
g.add_argument('--sha1',
help='take SHA1 of FILE and use that as an UUID',
metavar='FILE',
type=Path,
)
args = parser.parse_args(args[1:])
if args.uuid is None:
args.uuid = hashlib.sha1(args.sha1.read_bytes()).hexdigest()
if args.output is None:
args.output = args.loader
pre, meta, post = meta_re.match(args.loader.read_text()).groups()
meta = json.loads(meta)
meta['package_uuid'] = args.uuid
meta = json.dumps(meta, separators=(',', ':'), check_circular=False, ensure_ascii=False)
update_text_file(args.output, pre + meta + post)
if __name__ == '__main__':
run_main(main)

View file

@ -52,5 +52,6 @@ def main(args):
print("Generated package {}".format(str(args.output)))
if __name__ == '__main__':
run_main(main)

View file

@ -6,6 +6,7 @@ from taiseilib.common import (
)
from pathlib import Path
import fnmatch
def main(args):
@ -25,9 +26,12 @@ def main(args):
args = parser.parse_args(args[1:])
for pattern in args.patterns:
for path in args.directory.glob(pattern):
print(path.relative_to(args.directory))
for path in (p.relative_to(args.directory) for p in args.directory.rglob('*')):
path = str(path)
for pattern in args.patterns:
if fnmatch.fnmatchcase(path, pattern):
print(path)
break
if __name__ == '__main__':

View file

@ -125,3 +125,6 @@ glob_command = [python_thunk, glob_script]
check_submodules_script = files('check-submodules.py')
check_submodules_command = [python_thunk, check_submodules_script]
em_set_bundle_uuid_script = files('em-set-bundle-uuid.py')
em_set_bundle_uuid_command = [python_thunk, em_set_bundle_uuid_script]

View file

@ -42,7 +42,7 @@ validation_code = '''if validate_glsl
fname = '@0@'.format(src)
stage = fname.split('.')[-2]
glsl_targets += custom_target(fname.underscorify(),
spirv = custom_target('SPIRV_' + fname.underscorify(),
input : src,
output : '@BASENAME@.spv',
command : [
@ -53,6 +53,26 @@ validation_code = '''if validate_glsl
build_by_default : true,
depfile : '@0@.d'.format(fname.underscorify()),
)
spirv_targets += spirv
if transpile_glsl
essl = custom_target('ESSL_' + fname.underscorify(),
input : spirv,
output : '@BASENAME@.glsl',
command : [
spvc_command,
'--output', '@OUTPUT@', '@INPUT@',
spvc_args,
get_variable('spvc_@0@_args'.format(stage)),
],
install : false,
build_by_default : true,
depfile : '@0@.d'.format(fname.underscorify()),
)
essl_targets += essl
endif
endforeach
endif'''

View file

@ -200,4 +200,5 @@ int cli_args(int argc, char **argv, CLIAction *a) {
void free_cli_action(CLIAction *a) {
free(a->filename);
a->filename = NULL;
}

View file

@ -390,14 +390,24 @@ static bool config_set(const char *key, const char *val, void *data) {
typedef void (*ConfigUpgradeFunc)(void);
static void config_upgrade_1(void) {
// disable vsync by default
config_set_int(CONFIG_VSYNC, 0);
// reset vsync to the default value
config_set_int(CONFIG_VSYNC, CONFIG_VSYNC_DEFAULT);
// this version also changes meaning of the vsync value
// previously it was: 0 = on, 1 = off, 2 = adaptive, because lachs0r doesn't know how my absolutely genius options menu works.
// now it is: 0 = off, 1 = on, 2 = adaptive, as it should be.
}
#ifdef __EMSCRIPTEN__
static void config_upgrade_2(void) {
// emscripten defaults for these have been changed
config_set_int(CONFIG_VSYNC, CONFIG_VSYNC_DEFAULT);
config_set_int(CONFIG_FXAA, CONFIG_FXAA_DEFAULT);
}
#else
#define config_upgrade_2 NULL
#endif
static ConfigUpgradeFunc config_upgrades[] = {
/*
To bump the config version and add an upgrade state, simply append an upgrade function to this array.
@ -408,6 +418,7 @@ static ConfigUpgradeFunc config_upgrades[] = {
*/
config_upgrade_1,
config_upgrade_2,
};
static void config_apply_upgrades(int start) {

View file

@ -74,6 +74,15 @@
CONFIGDEF_GPKEYBINDING(KEY_SKIP, "gamepad_key_skip", GAMEPAD_BUTTON_B) \
#ifdef __EMSCRIPTEN__
#define CONFIG_VSYNC_DEFAULT 1
#define CONFIG_FXAA_DEFAULT 0
#else
#define CONFIG_VSYNC_DEFAULT 0
#define CONFIG_FXAA_DEFAULT 1
#endif
#define CONFIGDEFS \
/* @version must be on top. don't change its default value here, it does nothing. */ \
CONFIGDEF_INT (VERSION, "@version", 0) \
@ -85,7 +94,7 @@
CONFIGDEF_INT (VID_HEIGHT, "vid_height", RESY) \
CONFIGDEF_INT (VID_RESIZABLE, "vid_resizable", 0) \
CONFIGDEF_INT (VID_FRAMESKIP, "vid_frameskip", 1) \
CONFIGDEF_INT (VSYNC, "vsync", 0) \
CONFIGDEF_INT (VSYNC, "vsync", CONFIG_VSYNC_DEFAULT) \
CONFIGDEF_INT (MIXER_CHUNKSIZE, "mixer_chunksize", 1024) \
CONFIGDEF_FLOAT (SFX_VOLUME, "sfx_volume", 0.5) \
CONFIGDEF_FLOAT (BGM_VOLUME, "bgm_volume", 1.0) \
@ -100,7 +109,7 @@
CONFIGDEF_INT (SHOT_INVERTED, "shot_inverted", 0) \
CONFIGDEF_INT (FOCUS_LOSS_PAUSE, "focus_loss_pause", 1) \
CONFIGDEF_INT (PARTICLES, "particles", 1) \
CONFIGDEF_INT (FXAA, "fxaa", 1) \
CONFIGDEF_INT (FXAA, "fxaa", CONFIG_FXAA_DEFAULT) \
CONFIGDEF_INT (POSTPROCESS, "postprocess", 2) \
CONFIGDEF_INT (HEALTHBAR_STYLE, "healthbar_style", 1) \
CONFIGDEF_INT (SKIP_SPEED, "skip_speed", 10) \

View file

@ -27,6 +27,7 @@ static struct {
float panelalpha;
int end;
bool skipable;
CallChain cc;
} credits;
#define CREDITS_ENTRY_FADEIN 200.0
@ -417,7 +418,7 @@ void credits_preload(void) {
NULL);
}
static FrameAction credits_logic_frame(void *arg) {
static LogicFrameAction credits_logic_frame(void *arg) {
update_transition();
events_poll(NULL, 0);
credits_process();
@ -432,14 +433,19 @@ static FrameAction credits_logic_frame(void *arg) {
}
}
static FrameAction credits_render_frame(void *arg) {
static RenderFrameAction credits_render_frame(void *arg) {
credits_draw();
return RFRAME_SWAP;
}
void credits_loop(void) {
static void credits_end_loop(void *ctx) {
credits_free();
run_call_chain(&credits.cc, NULL);
}
void credits_enter(CallChain next) {
credits_preload();
credits_init();
loop_at_fps(credits_logic_frame, credits_render_frame, NULL, FPS);
credits_free();
credits.cc = next;
eventloop_enter(&credits, credits_logic_frame, credits_render_frame, credits_end_loop, FPS);
}

View file

@ -11,7 +11,9 @@
#include "taisei.h"
void credits_loop(void);
#include "eventloop/eventloop.h"
void credits_enter(CallChain next);
void credits_preload(void);
#endif // IGUARD_credits_h

View file

@ -13,18 +13,19 @@
#include "video.h"
#include "progress.h"
struct EndingEntry {
typedef struct EndingEntry {
char *msg;
Sprite *sprite;
int time;
};
} EndingEntry;
struct Ending {
typedef struct Ending {
EndingEntry *entries;
int count;
int duration;
int pos;
};
CallChain cc;
} Ending;
static void track_ending(int ending) {
assert(ending >= 0 && ending < NUM_ENDINGS);
@ -150,9 +151,7 @@ void good_ending_reimu(Ending *e) {
track_ending(ENDING_GOOD_3);
}
static void create_ending(Ending *e) {
memset(e, 0, sizeof(Ending));
static void init_ending(Ending *e) {
if(global.plr.continues_used) {
global.plr.mode->character->ending.bad(e);
} else {
@ -234,7 +233,7 @@ static bool ending_input_handler(SDL_Event *event, void *arg) {
return false;
}
static FrameAction ending_logic_frame(void *arg) {
static LogicFrameAction ending_logic_frame(void *arg) {
Ending *e = arg;
update_transition();
@ -267,7 +266,7 @@ static FrameAction ending_logic_frame(void *arg) {
return LFRAME_WAIT;
}
static FrameAction ending_render_frame(void *arg) {
static RenderFrameAction ending_render_frame(void *arg) {
Ending *e = arg;
if(e->pos < e->count-1)
@ -277,13 +276,24 @@ static FrameAction ending_render_frame(void *arg) {
return RFRAME_SWAP;
}
void ending_loop(void) {
Ending e;
static void ending_loop_end(void *ctx) {
Ending *e = ctx;
CallChain cc = e->cc;
free_ending(e);
free(e);
run_call_chain(&cc, NULL);
}
void ending_enter(CallChain next) {
ending_preload();
create_ending(&e);
Ending *e = calloc(1, sizeof(Ending));
init_ending(e);
e->cc = next;
global.frames = 0;
set_ortho(SCREEN_W, SCREEN_H);
start_bgm("ending");
loop_at_fps(ending_logic_frame, ending_render_frame, &e, FPS);
free_ending(&e);
eventloop_enter(e, ending_logic_frame, ending_render_frame, ending_loop_end, FPS);
}

View file

@ -11,7 +11,7 @@
#include "taisei.h"
#include "resource/texture.h"
#include "eventloop/eventloop.h"
enum {
ENDING_FADE_OUT = 200,
@ -56,10 +56,9 @@ static inline attr_must_inline bool ending_is_bad(EndingID end) {
#undef ENDING
}
typedef struct EndingEntry EndingEntry;
typedef struct Ending Ending;
void ending_loop(void);
void ending_enter(CallChain next);
void ending_preload(void);
/*

114
src/eventloop/eventloop.c Normal file
View file

@ -0,0 +1,114 @@
/*
* 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@alienslab.net>.
*/
#include "taisei.h"
#include "eventloop_private.h"
#include "util.h"
#include "global.h"
#include "video.h"
struct evloop_s evloop;
void eventloop_enter(void *context, LogicFrameFunc frame_logic, RenderFrameFunc frame_render, PostLoopFunc on_leave, uint target_fps) {
assert(is_main_thread());
assume(evloop.stack_ptr < evloop.stack + EVLOOP_STACK_SIZE - 1);
LoopFrame *frame;
if(evloop.stack_ptr == NULL) {
frame = evloop.stack_ptr = evloop.stack;
} else {
frame = ++evloop.stack_ptr;
}
frame->context = context;
frame->logic = frame_logic;
frame->render = frame_render;
frame->on_leave = on_leave;
frame->frametime = HRTIME_RESOLUTION / target_fps;
frame->prev_logic_action = LFRAME_WAIT;
}
void eventloop_leave(void) {
assert(is_main_thread());
assume(evloop.stack_ptr != NULL);
LoopFrame *frame = evloop.stack_ptr;
if(evloop.stack_ptr == evloop.stack) {
evloop.stack_ptr = NULL;
} else {
--evloop.stack_ptr;
}
if(frame->on_leave != NULL) {
frame->on_leave(frame->context);
}
}
LogicFrameAction run_logic_frame(LoopFrame *frame) {
assert(frame == evloop.stack_ptr);
if(frame->prev_logic_action == LFRAME_STOP) {
return LFRAME_STOP;
}
LogicFrameAction a = frame->logic(frame->context);
if(taisei_quit_requested()) {
a = LFRAME_STOP;
}
frame->prev_logic_action = a;
return a;
}
LogicFrameAction handle_logic(LoopFrame **pframe, const FrameTimes *ftimes) {
LogicFrameAction lframe_action;
uint cnt = 0;
do {
lframe_action = run_logic_frame(*pframe);
while(evloop.stack_ptr != *pframe) {
*pframe = evloop.stack_ptr;
lframe_action = run_logic_frame(*pframe);
cnt = UINT_MAX; // break out of the outer loop
}
} while(
lframe_action == LFRAME_SKIP &&
++cnt < config_get_int(CONFIG_SKIP_SPEED)
);
if(lframe_action == LFRAME_STOP) {
eventloop_leave();
*pframe = evloop.stack_ptr;
if(*pframe == NULL) {
return LFRAME_STOP;
}
}
fpscounter_update(&global.fps.logic);
return lframe_action;
}
RenderFrameAction run_render_frame(LoopFrame *frame) {
attr_unused LoopFrame *stack_prev = evloop.stack_ptr;
r_framebuffer_clear(NULL, CLEAR_ALL, RGBA(0, 0, 0, 1), 1);
RenderFrameAction a = frame->render(frame->context);
assert(evloop.stack_ptr == stack_prev);
if(a == RFRAME_SWAP) {
video_swap_buffers();
}
fpscounter_update(&global.fps.render);
return a;
}

93
src/eventloop/eventloop.h Normal file
View file

@ -0,0 +1,93 @@
/*
* 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@alienslab.net>.
*/
#ifndef IGUARD_eventloop_eventloop_h
#define IGUARD_eventloop_eventloop_h
#include "taisei.h"
typedef enum RenderFrameAction {
RFRAME_SWAP,
RFRAME_DROP,
} RenderFrameAction;
typedef enum LogicFrameAction {
LFRAME_WAIT,
LFRAME_SKIP,
LFRAME_STOP,
} LogicFrameAction;
typedef LogicFrameAction (*LogicFrameFunc)(void *context);
typedef RenderFrameAction (*RenderFrameFunc)(void *context);
typedef void (*PostLoopFunc)(void *context);
#ifdef DEBUG_CALLCHAIN
#include "util/debug.h"
#include "log.h"
#endif
typedef struct CallChainResult {
void *ctx;
void *result;
} CallChainResult;
typedef struct CallChain {
void (*callback)(CallChainResult result);
void *ctx;
#ifdef DEBUG_CALLCHAIN
DebugInfo _debug_;
#endif
} CallChain;
#ifdef DEBUG_CALLCHAIN
#define CALLCHAIN(callback, ctx) ((CallChain) { (callback), (ctx), _DEBUG_INFO_INITIALIZER_ })
#else
#define CALLCHAIN(callback, ctx) ((CallChain) { (callback), (ctx) })
#endif
#define NO_CALLCHAIN CALLCHAIN(NULL, NULL)
#define CALLCHAIN_RESULT(ctx, result) ((CallChainResult) { (ctx), (result) })
void eventloop_enter(
void *context,
LogicFrameFunc frame_logic,
RenderFrameFunc frame_render,
PostLoopFunc on_leave,
uint target_fps
) attr_nonnull(1, 2, 3);
void eventloop_run(void);
#ifdef DEBUG_CALLCHAIN
attr_must_inline
static inline void run_call_chain(CallChain *cc, void *result, DebugInfo caller_dbg) {
if(cc->callback != NULL) {
log_debug("Calling CC set in %s (%s:%u)",
cc->_debug_.func, cc->_debug_.file, cc->_debug_.line);
log_debug(" from %s (%s:%u)",
caller_dbg.func, caller_dbg.file, caller_dbg.line);
cc->callback(CALLCHAIN_RESULT(cc->ctx, result));
} else {
log_debug("Dead end at %s (%s:%u)",
caller_dbg.func, caller_dbg.file, caller_dbg.line
);
}
}
#define run_call_chain(cc, result) run_call_chain(cc, result, _DEBUG_INFO_)
#else
attr_must_inline
static inline void run_call_chain(CallChain *cc, void *result) {
if(cc->callback != NULL) {
cc->callback(CALLCHAIN_RESULT(cc->ctx, result));
}
}
#endif
#endif // IGUARD_eventloop_eventloop_h

View file

@ -0,0 +1,47 @@
/*
* 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@alienslab.net>.
*/
#ifndef IGUARD_eventloop_eventloop_private_h
#define IGUARD_eventloop_eventloop_private_h
#include "taisei.h"
#include "eventloop.h"
#include "hirestime.h"
typedef struct LoopFrame LoopFrame;
#define EVLOOP_STACK_SIZE 32
struct LoopFrame {
void *context;
LogicFrameFunc logic;
RenderFrameFunc render;
PostLoopFunc on_leave;
hrtime_t frametime;
LogicFrameAction prev_logic_action;
};
extern struct evloop_s {
LoopFrame stack[EVLOOP_STACK_SIZE];
LoopFrame *stack_ptr;
} evloop;
typedef struct FrameTimes {
hrtime_t target;
hrtime_t start;
hrtime_t next;
} FrameTimes;
void eventloop_leave(void);
LogicFrameAction run_logic_frame(LoopFrame *frame);
LogicFrameAction handle_logic(LoopFrame **pframe, const FrameTimes *ftimes);
RenderFrameAction run_render_frame(LoopFrame *frame);
#endif // IGUARD_eventloop_eventloop_private_h

View file

@ -0,0 +1,105 @@
/*
* 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@alienslab.net>.
*/
#include "taisei.h"
#include "eventloop_private.h"
#include "events.h"
#include "global.h"
#include <emscripten.h>
static FrameTimes frame_times;
static uint frame_num;
static bool em_handle_resize_event(SDL_Event *event, void *arg);
static void em_loop_callback(void) {
LoopFrame *frame = evloop.stack_ptr;
if(!frame) {
events_unregister_handler(em_handle_resize_event);
emscripten_cancel_main_loop();
return;
}
if(time_get() < frame_times.next) {
return;
}
frame_times.start = time_get();
frame_times.target = frame->frametime;
frame_times.next += frame_times.target;
hrtime_t min_next_time = frame_times.start - 2 * frame_times.target;
if(min_next_time > frame_times.next) {
frame_times.next = min_next_time;
}
global.fps.busy.last_update_time = frame_times.start;
LogicFrameAction lframe_action = handle_logic(&frame, &frame_times);
if(!frame || lframe_action == LFRAME_STOP) {
return;
}
if(!(frame_num++ % get_effective_frameskip())) {
run_render_frame(frame);
}
fpscounter_update(&global.fps.busy);
}
static void update_vsync(void) {
switch(config_get_int(CONFIG_VSYNC)) {
case 0: r_vsync(VSYNC_NONE); break;
default: r_vsync(VSYNC_NORMAL); break;
}
}
static bool em_handle_resize_event(SDL_Event *event, void *arg) {
emscripten_cancel_main_loop();
emscripten_set_main_loop(em_loop_callback, 0, false);
update_vsync();
return false;
}
static bool em_audio_workaround(SDL_Event *event, void *arg) {
// Workaround for Chromium:
// https://github.com/emscripten-core/emscripten/issues/6511
// Will start playing audio as soon as the first input occurs.
(__extension__ EM_ASM({
var audioctx = Module['SDL2'].audioContext;
if(audioctx !== undefined) {
audioctx.resume();
}
}));
events_unregister_handler(em_audio_workaround);
return false;
}
void eventloop_run(void) {
frame_times.next = time_get();
emscripten_set_main_loop(em_loop_callback, 0, false);
update_vsync();
events_register_handler(&(EventHandler) {
em_audio_workaround, NULL, EPRIO_SYSTEM, SDL_KEYDOWN,
});
events_register_handler(&(EventHandler) {
em_handle_resize_event, NULL, EPRIO_SYSTEM, MAKE_TAISEI_EVENT(TE_VIDEO_MODE_CHANGED)
});
(__extension__ EM_ASM({
Module['onFirstFrame']();
}));
}

View file

@ -0,0 +1,139 @@
/*
* 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@alienslab.net>.
*/
#include "taisei.h"
#include "eventloop_private.h"
#include "util.h"
#include "framerate.h"
#include "global.h"
void eventloop_run(void) {
assert(is_main_thread());
if(evloop.stack_ptr == NULL) {
return;
}
LoopFrame *frame = evloop.stack_ptr;
FrameTimes frame_times;
frame_times.target = frame->frametime;
frame_times.start = time_get();
frame_times.next = frame_times.start + frame_times.target;
int32_t sleep = env_get("TAISEI_FRAMELIMITER_SLEEP", 3);
bool compensate = env_get("TAISEI_FRAMELIMITER_COMPENSATE", 1);
bool uncapped_rendering_env, uncapped_rendering;
if(global.is_replay_verification) {
uncapped_rendering_env = false;
} else {
uncapped_rendering_env = env_get("TAISEI_FRAMELIMITER_LOGIC_ONLY", 0);
}
uncapped_rendering = uncapped_rendering_env;
uint32_t frame_num = 0;
begin_main_loop:
while(frame != NULL) {
#ifdef DEBUG
if(uncapped_rendering_env) {
uncapped_rendering = !gamekeypressed(KEY_FPSLIMIT_OFF);
}
#endif
frame_times.start = time_get();
begin_frame:
global.fps.busy.last_update_time = time_get();
frame_times.target = frame->frametime;
++frame_num;
LogicFrameAction lframe_action = LFRAME_WAIT;
if(uncapped_rendering) {
attr_unused uint32_t logic_frames = 0;
while(lframe_action != LFRAME_STOP && frame_times.next < frame_times.start) {
lframe_action = handle_logic(&frame, &frame_times);
if(!frame || lframe_action == LFRAME_STOP) {
goto begin_main_loop;
}
++logic_frames;
hrtime_t total = time_get() - frame_times.start;
if(total > frame_times.target) {
frame_times.next = frame_times.start;
log_debug("Executing logic took too long (%"PRIuTIME"), giving up", total);
} else {
frame_times.next += frame_times.target;
}
}
if(logic_frames > 1) {
log_debug(
"Dropped %u logic frame%s in superframe #%u",
logic_frames - 1,
logic_frames > 2 ? "s" : "",
frame_num
);
}
} else {
lframe_action = handle_logic(&frame, &frame_times);
if(!frame || lframe_action == LFRAME_STOP) {
goto begin_main_loop;
}
}
if((uncapped_rendering || !(frame_num % get_effective_frameskip())) && !global.is_replay_verification) {
run_render_frame(frame);
}
fpscounter_update(&global.fps.busy);
if(uncapped_rendering || global.frameskip > 0 || global.is_replay_verification) {
continue;
}
#ifdef DEBUG
if(gamekeypressed(KEY_FPSLIMIT_OFF)) {
continue;
}
#endif
frame_times.next = frame_times.start + frame_times.target;
if(compensate) {
hrtime_t rt = time_get();
if(rt > frame_times.next) {
// frame took too long...
// try to compensate in the next frame to avoid slowdown
frame_times.start = rt - imin(rt - frame_times.next, frame_times.target);
goto begin_frame;
}
}
if(sleep > 0) {
// CAUTION: All of these casts are important!
while((shrtime_t)frame_times.next - (shrtime_t)time_get() > (shrtime_t)frame_times.target / sleep) {
uint32_t nap_multiplier = 1;
uint32_t nap_divisor = 3;
hrtime_t nap_raw = imax(0, (shrtime_t)frame_times.next - (shrtime_t)time_get());
uint32_t nap_sdl = (nap_multiplier * nap_raw * 1000) / (HRTIME_RESOLUTION * nap_divisor);
nap_sdl = imax(nap_sdl, 1);
SDL_Delay(nap_sdl);
}
}
while(time_get() < frame_times.next);
}
}

14
src/eventloop/meson.build Normal file
View file

@ -0,0 +1,14 @@
eventloop_src = files(
'eventloop.c',
)
if host_machine.system() == 'emscripten'
eventloop_src += files(
'executor_emscripten.c',
)
else
eventloop_src += files(
'executor_synchro.c',
)
endif

View file

@ -299,8 +299,74 @@ static bool events_handler_hotkeys(SDL_Event *event, void *arg);
static bool events_handler_key_down(SDL_Event *event, void *arg);
static bool events_handler_key_up(SDL_Event *event, void *arg);
attr_unused
static bool events_handler_debug_winevt(SDL_Event *event, void *arg) {
// copied from SDL wiki almost verbatim
if(event->type == SDL_WINDOWEVENT) {
switch(event->window.event) {
case SDL_WINDOWEVENT_SHOWN:
log_info("Window %d shown", event->window.windowID);
break;
case SDL_WINDOWEVENT_HIDDEN:
log_info("Window %d hidden", event->window.windowID);
break;
case SDL_WINDOWEVENT_EXPOSED:
log_info("Window %d exposed", event->window.windowID);
break;
case SDL_WINDOWEVENT_MOVED:
log_info("Window %d moved to %d,%d", event->window.windowID, event->window.data1, event->window.data2);
break;
case SDL_WINDOWEVENT_RESIZED:
log_info("Window %d resized to %dx%d", event->window.windowID, event->window.data1, event->window.data2);
break;
case SDL_WINDOWEVENT_SIZE_CHANGED:
log_info("Window %d size changed to %dx%d", event->window.windowID, event->window.data1, event->window.data2);
break;
case SDL_WINDOWEVENT_MINIMIZED:
log_info("Window %d minimized", event->window.windowID);
break;
case SDL_WINDOWEVENT_MAXIMIZED:
log_info("Window %d maximized", event->window.windowID);
break;
case SDL_WINDOWEVENT_RESTORED:
log_info("Window %d restored", event->window.windowID);
break;
case SDL_WINDOWEVENT_ENTER:
log_info("Mouse entered window %d", event->window.windowID);
break;
case SDL_WINDOWEVENT_LEAVE:
log_info("Mouse left window %d", event->window.windowID);
break;
case SDL_WINDOWEVENT_FOCUS_GAINED:
log_info("Window %d gained keyboard focus", event->window.windowID);
break;
case SDL_WINDOWEVENT_FOCUS_LOST:
log_info("Window %d lost keyboard focus", event->window.windowID);
break;
case SDL_WINDOWEVENT_CLOSE:
log_info("Window %d closed", event->window.windowID);
break;
case SDL_WINDOWEVENT_TAKE_FOCUS:
log_info("Window %d is offered a focus", event->window.windowID);
break;
case SDL_WINDOWEVENT_HIT_TEST:
log_info("Window %d has a special hit test", event->window.windowID);
break;
default:
log_warn("Window %d got unknown event %d", event->window.windowID, event->window.event);
break;
}
}
return false;
}
static EventHandler default_handlers[] = {
#ifdef DEBUG_WINDOW_EVENTS
{ .proc = events_handler_debug_winevt, .priority = EPRIO_SYSTEM, .event_type = SDL_WINDOWEVENT },
#endif
{ .proc = events_handler_quit, .priority = EPRIO_SYSTEM, .event_type = SDL_QUIT },
{ .proc = events_handler_config, .priority = EPRIO_SYSTEM, .event_type = 0 },
{ .proc = events_handler_keyrepeat_workaround, .priority = EPRIO_CAPTURE, .event_type = 0 },
@ -394,6 +460,10 @@ static bool events_handler_key_down(SDL_Event *event, void *arg) {
SDL_Scancode scan = event->key.keysym.scancode;
bool repeat = event->key.repeat;
if(video.backend == VIDEO_BACKEND_EMSCRIPTEN && scan == SDL_SCANCODE_TAB) {
scan = SDL_SCANCODE_ESCAPE;
}
/*
* Emit menu events
*/
@ -477,7 +547,7 @@ static bool events_handler_hotkeys(SDL_Event *event, void *arg) {
}
if((scan == SDL_SCANCODE_RETURN && (mod & KMOD_ALT)) || scan == config_get_int(CONFIG_KEY_FULLSCREEN)) {
config_set_int(CONFIG_FULLSCREEN, !config_get_int(CONFIG_FULLSCREEN));
video_set_fullscreen(!video_is_fullscreen());
return true;
}

View file

@ -54,163 +54,3 @@ uint32_t get_effective_frameskip(void) {
return frameskip;
}
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#define SDL_Delay emscripten_sleep
#endif
void loop_at_fps(LogicFrameFunc logic_frame, RenderFrameFunc render_frame, void *arg, uint32_t fps) {
assert(logic_frame != NULL);
assert(render_frame != NULL);
assert(fps > 0);
hrtime_t frame_start_time = time_get();
hrtime_t next_frame_time = frame_start_time;
hrtime_t target_frame_time = HRTIME_RESOLUTION / fps;
FrameAction rframe_action = RFRAME_SWAP;
FrameAction lframe_action = LFRAME_WAIT;
int32_t sleep = env_get("TAISEI_FRAMELIMITER_SLEEP", 3);
bool compensate = env_get("TAISEI_FRAMELIMITER_COMPENSATE", 1);
bool uncapped_rendering_env = env_get("TAISEI_FRAMELIMITER_LOGIC_ONLY", 0);
if(global.is_replay_verification) {
uncapped_rendering_env = false;
}
uint32_t frame_num = 0;
// don't care about thread safety, we can render only on the main thread anyway
static uint8_t recursion_detector;
++recursion_detector;
while(true) {
bool uncapped_rendering = uncapped_rendering_env;
frame_start_time = time_get();
begin_frame:
global.fps.busy.last_update_time = time_get();
++frame_num;
if(uncapped_rendering) {
uint32_t logic_frames = 0;
while(lframe_action != LFRAME_STOP && next_frame_time < frame_start_time) {
uint8_t rval = recursion_detector;
lframe_action = logic_frame(arg);
fpscounter_update(&global.fps.logic);
++logic_frames;
if(rval != recursion_detector) {
log_debug(
"Recursive call detected (%u != %u), resetting next frame time to avoid a large skip",
rval,
recursion_detector
);
next_frame_time = time_get() + target_frame_time;
break;
}
hrtime_t frametime = target_frame_time;
if(lframe_action == LFRAME_SKIP) {
frametime /= imax(1, config_get_int(CONFIG_SKIP_SPEED));
}
next_frame_time += frametime;
hrtime_t total = time_get() - frame_start_time;
if(total > target_frame_time) {
next_frame_time = frame_start_time;
log_debug("Executing logic took too long (%"PRIuTIME"), giving up", total);
}
}
if(logic_frames > 1) {
log_debug(
"Dropped %u logic frame%s in superframe #%u",
logic_frames - 1,
logic_frames > 2 ? "s" : "",
frame_num
);
}
} else {
uint cnt = 0;
do {
lframe_action = logic_frame(arg);
fpscounter_update(&global.fps.logic);
} while(lframe_action == LFRAME_SKIP && ++cnt < config_get_int(CONFIG_SKIP_SPEED));
}
if(taisei_quit_requested()) {
break;
}
if((!uncapped_rendering && frame_num % get_effective_frameskip()) || global.is_replay_verification) {
rframe_action = RFRAME_DROP;
} else {
r_framebuffer_clear(NULL, CLEAR_ALL, RGBA(0, 0, 0, 1), 1);
rframe_action = render_frame(arg);
fpscounter_update(&global.fps.render);
}
if(rframe_action == RFRAME_SWAP) {
video_swap_buffers();
}
if(lframe_action == LFRAME_STOP) {
break;
}
fpscounter_update(&global.fps.busy);
if(uncapped_rendering || global.frameskip > 0) {
continue;
}
#ifdef DEBUG
if(gamekeypressed(KEY_FPSLIMIT_OFF)) {
continue;
}
#endif
next_frame_time = frame_start_time + target_frame_time;
// next_frame_time = frame_start_time + 2 * target_frame_time - global.fps.logic.frametime;
#ifdef __EMSCRIPTEN__
SDL_Delay(1);
#endif
if(compensate) {
hrtime_t rt = time_get();
if(rt > next_frame_time) {
// frame took too long...
// try to compensate in the next frame to avoid slowdown
frame_start_time = rt - imin(rt - next_frame_time, target_frame_time);
goto begin_frame;
}
}
if(sleep > 0) {
// CAUTION: All of these casts are important!
while((shrtime_t)next_frame_time - (shrtime_t)time_get() > (shrtime_t)target_frame_time / sleep) {
uint32_t nap_multiplier = 1;
uint32_t nap_divisor = 3;
hrtime_t nap_raw = imax(0, (shrtime_t)next_frame_time - (shrtime_t)time_get());
uint32_t nap_sdl = (nap_multiplier * nap_raw * 1000) / (HRTIME_RESOLUTION * nap_divisor);
nap_sdl = imax(nap_sdl, 1);
SDL_Delay(nap_sdl);
}
}
while(time_get() < next_frame_time);
}
}

View file

@ -20,20 +20,7 @@ typedef struct {
hrtime_t last_update_time; // internal; last time the average was recalculated
} FPSCounter;
typedef enum FrameAction {
RFRAME_SWAP,
RFRAME_DROP,
LFRAME_WAIT,
LFRAME_SKIP,
LFRAME_STOP,
} FrameAction;
typedef FrameAction (*LogicFrameFunc)(void*);
typedef FrameAction (*RenderFrameFunc)(void*);
uint32_t get_effective_frameskip(void);
void loop_at_fps(LogicFrameFunc logic_frame, RenderFrameFunc render_frame, void *arg, uint32_t fps);
void fpscounter_reset(FPSCounter *fps);
void fpscounter_update(FPSCounter *fps);

View file

@ -55,3 +55,9 @@ void taisei_quit(void) {
bool taisei_quit_requested(void) {
return SDL_AtomicGet(&quitting);
}
void taisei_commit_persistent_data(void) {
config_save();
progress_save();
vfs_sync(VFS_SYNC_STORE, NO_CALLCHAIN);
}

View file

@ -138,6 +138,7 @@ void init_global(CLIAction *cli);
void taisei_quit(void);
bool taisei_quit_requested(void);
void taisei_commit_persistent_data(void);
// XXX: Move this somewhere?
bool gamekeypressed(KeyIndex key);

View file

@ -19,13 +19,13 @@ typedef struct ListAnchorInterface ListAnchorInterface;
typedef struct ListAnchor ListAnchor;
typedef struct ListContainer ListContainer;
#define LIST_INTERFACE_BASE(typename) LIST_ALIGN struct { \
#define LIST_INTERFACE_BASE(typename) struct { \
typename *next; \
typename *prev; \
}
#define LIST_INTERFACE(typename) union { \
ListInterface list_interface; \
LIST_ALIGN ListInterface list_interface; \
LIST_INTERFACE_BASE(typename); \
}

View file

@ -50,6 +50,8 @@ typedef enum LogLevel {
#ifndef LOG_DEFAULT_LEVELS_CONSOLE
#ifdef DEBUG
#define LOG_DEFAULT_LEVELS_CONSOLE LOG_ALL
#elif defined __EMSCRIPTEN__
#define LOG_DEFAULT_LEVELS_CONSOLE LOG_ALERT | LOG_INFO
#else
#define LOG_DEFAULT_LEVELS_CONSOLE LOG_ALERT
#endif

View file

@ -24,9 +24,9 @@
#include "vfs/setup.h"
#include "version.h"
#include "credits.h"
#include "renderer/api.h"
#include "taskmanager.h"
attr_unused
static void taisei_shutdown(void) {
log_info("Shutting down");
@ -144,76 +144,82 @@ static void log_system_specs(void) {
log_info("RAM: %d MB", SDL_GetSystemRAM());
}
static void log_version(void) {
log_info("%s %s", TAISEI_VERSION_FULL, TAISEI_VERSION_BUILD_TYPE);
}
typedef struct MainContext {
CLIAction cli;
Replay replay;
int replay_idx;
uchar headless : 1;
} MainContext;
static void main_post_vfsinit(CallChainResult ccr);
static void main_singlestg(MainContext *mctx) attr_unused;
static void main_replay(MainContext *mctx);
static noreturn void main_vfstree(CallChainResult ccr);
static noreturn void main_quit(MainContext *ctx, int status) {
free_cli_action(&ctx->cli);
replay_destroy(&ctx->replay);
free(ctx);
exit(status);
}
int main(int argc, char **argv) {
MainContext *ctx = calloc(1, sizeof(*ctx));
setlocale(LC_ALL, "C");
Replay replay = {0};
int replay_idx = 0;
bool headless = false;
htutil_init();
init_log();
stage_init_array(); // cli_args depends on this
// commandline arguments should be parsed as early as possible
CLIAction a;
cli_args(argc, argv, &a); // stage_init_array goes first!
cli_args(argc, argv, &ctx->cli); // stage_init_array goes first!
if(a.type == CLI_Quit) {
free_cli_action(&a);
return 1;
if(ctx->cli.type == CLI_Quit) {
main_quit(ctx, 0);
}
if(a.type == CLI_DumpStages) {
if(ctx->cli.type == CLI_DumpStages) {
for(StageInfo *stg = stages; stg->procs; ++stg) {
tsfprintf(stdout, "%X %s: %s\n", stg->id, stg->title, stg->subtitle);
}
free_cli_action(&a);
return 0;
} else if(a.type == CLI_PlayReplay || a.type == CLI_VerifyReplay) {
if(!replay_load_syspath(&replay, a.filename, REPLAY_READ_ALL)) {
free_cli_action(&a);
return 1;
}
replay_idx = a.stageid ? replay_find_stage_idx(&replay, a.stageid) : 0;
if(replay_idx < 0) {
free_cli_action(&a);
return 1;
}
if(a.type == CLI_VerifyReplay) {
headless = true;
}
} else if(a.type == CLI_DumpVFSTree) {
vfs_setup(false);
SDL_RWops *rwops = SDL_RWFromFP(stdout, false);
int status = 0;
if(!rwops) {
log_fatal("SDL_RWFromFP() failed: %s", SDL_GetError());
log_sdl_error(LOG_ERROR, "SDL_RWFromFP");
status = 1;
} else if(!vfs_print_tree(rwops, a.filename)) {
log_error("VFS error: %s", vfs_get_error());
status = 2;
}
SDL_RWclose(rwops);
vfs_shutdown();
free_cli_action(&a);
return status;
main_quit(ctx, 0);
}
free_cli_action(&a);
if(ctx->cli.type == CLI_PlayReplay || ctx->cli.type == CLI_VerifyReplay) {
if(!replay_load_syspath(&ctx->replay, ctx->cli.filename, REPLAY_READ_ALL)) {
main_quit(ctx, 1);
}
vfs_setup(false);
ctx->replay_idx = ctx->cli.stageid ? replay_find_stage_idx(&ctx->replay, ctx->cli.stageid) : 0;
if(headless) {
if(ctx->replay_idx < 0) {
main_quit(ctx, 1);
}
if(ctx->cli.type == CLI_VerifyReplay) {
ctx->headless = true;
}
} else if(ctx->cli.type == CLI_DumpVFSTree) {
vfs_setup(CALLCHAIN(main_vfstree, ctx));
return 0; // NO main_quit here! vfs_setup may be asynchronous.
}
log_info("Girls are now preparing, please wait warmly...");
free_cli_action(&ctx->cli);
vfs_setup(CALLCHAIN(main_post_vfsinit, ctx));
return 0;
}
static void main_post_vfsinit(CallChainResult ccr) {
MainContext *ctx = ccr.ctx;
if(ctx->headless) {
env_set("SDL_AUDIODRIVER", "dummy", true);
env_set("SDL_VIDEODRIVER", "dummy", true);
env_set("TAISEI_AUDIO_BACKEND", "null", true);
@ -224,12 +230,7 @@ int main(int argc, char **argv) {
init_log_file();
}
#ifdef __EMSCRIPTEN__
env_set("TAISEI_NOASYNC", true, true);
env_set("TAISEI_NOPRELOAD", true, true);
#endif
log_info("%s %s", TAISEI_VERSION_FULL, TAISEI_VERSION_BUILD_TYPE);
log_version();
log_system_specs();
log_lib_versions();
@ -238,7 +239,7 @@ int main(int argc, char **argv) {
init_sdl();
taskmgr_global_init();
time_init();
init_global(&a);
init_global(&ctx->cli);
events_init();
video_init();
init_resources();
@ -254,64 +255,120 @@ int main(int argc, char **argv) {
log_info("Initialization complete");
#ifndef __EMSCRIPTEN__
atexit(taisei_shutdown);
#endif
if(a.type == CLI_PlayReplay || a.type == CLI_VerifyReplay) {
replay_play(&replay, replay_idx);
replay_destroy(&replay);
return 0;
if(ctx->cli.type == CLI_PlayReplay || ctx->cli.type == CLI_VerifyReplay) {
main_replay(ctx);
return;
}
if(a.type == CLI_Credits) {
credits_loop();
return 0;
if(ctx->cli.type == CLI_Credits) {
credits_enter(NO_CALLCHAIN);
eventloop_run();
return;
}
#ifdef DEBUG
log_warn("Compiled with DEBUG flag!");
if(a.type == CLI_SelectStage) {
log_info("Entering stage skip mode: Stage %X", a.stageid);
StageInfo* stg = stage_get(a.stageid);
assert(stg); // properly checked before this
global.diff = stg->difficulty;
global.is_practice_mode = (stg->type != STAGE_EXTRA);
if(a.diff) {
global.diff = a.diff;
log_info("Setting difficulty to %s", difficulty_name(global.diff));
} else if(!global.diff) {
global.diff = D_Easy;
}
log_info("Entering %s", stg->title);
do {
global.replay_stage = NULL;
replay_init(&global.replay);
global.gameover = 0;
player_init(&global.plr);
if(a.plrmode) {
global.plr.mode = a.plrmode;
}
stage_loop(stg);
if(global.gameover == GAMEOVER_RESTART) {
replay_destroy(&global.replay);
}
} while(global.gameover == GAMEOVER_RESTART);
ask_save_replay();
return 0;
if(ctx->cli.type == CLI_SelectStage) {
main_singlestg(ctx);
return;
}
#endif
MenuData menu;
create_main_menu(&menu);
menu_loop(&menu);
return 0;
enter_menu(create_main_menu(), NO_CALLCHAIN);
eventloop_run();
}
typedef struct SingleStageContext {
PlayerMode *plrmode;
StageInfo *stg;
} SingleStageContext;
static void main_singlestg_begin_game(CallChainResult ccr);
static void main_singlestg_end_game(CallChainResult ccr);
static void main_singlestg_cleanup(CallChainResult ccr);
static void main_singlestg_begin_game(CallChainResult ccr) {
SingleStageContext *ctx = ccr.ctx;
global.replay_stage = NULL;
replay_init(&global.replay);
global.gameover = 0;
player_init(&global.plr);
if(ctx->plrmode) {
global.plr.mode = ctx->plrmode;
}
stage_enter(ctx->stg, CALLCHAIN(main_singlestg_end_game, ctx));
}
static void main_singlestg_end_game(CallChainResult ccr) {
if(global.gameover == GAMEOVER_RESTART) {
replay_destroy(&global.replay);
main_singlestg_begin_game(ccr);
} else {
ask_save_replay(CALLCHAIN(main_singlestg_cleanup, ccr.ctx));
}
}
static void main_singlestg_cleanup(CallChainResult ccr) {
replay_destroy(&global.replay);
free(ccr.ctx);
}
static void main_singlestg(MainContext *mctx) {
CLIAction *a = &mctx->cli;
log_info("Entering stage skip mode: Stage %X", a->stageid);
StageInfo* stg = stage_get(a->stageid);
assert(stg); // properly checked before this
SingleStageContext *ctx = calloc(1, sizeof(*ctx));
ctx->plrmode = a->plrmode;
ctx->stg = stg;
global.diff = stg->difficulty;
global.is_practice_mode = (stg->type != STAGE_EXTRA);
if(a->diff) {
global.diff = a->diff;
log_info("Setting difficulty to %s", difficulty_name(global.diff));
} else if(!global.diff) {
global.diff = D_Easy;
}
log_info("Entering %s", stg->title);
main_singlestg_begin_game(CALLCHAIN_RESULT(ctx, NULL));
eventloop_run();
}
static void main_replay(MainContext *mctx) {
replay_play(&mctx->replay, mctx->replay_idx, NO_CALLCHAIN);
replay_destroy(&mctx->replay); // replay_play makes a copy
eventloop_run();
}
static void main_vfstree(CallChainResult ccr) {
MainContext *mctx = ccr.ctx;
SDL_RWops *rwops = SDL_RWFromFP(stdout, false);
int status = 0;
if(!rwops) {
log_fatal("SDL_RWFromFP() failed: %s", SDL_GetError());
log_sdl_error(LOG_ERROR, "SDL_RWFromFP");
status = 1;
} else if(!vfs_print_tree(rwops, mctx->cli.filename)) {
log_error("VFS error: %s", vfs_get_error());
status = 2;
}
SDL_RWclose(rwops);
vfs_shutdown();
main_quit(mctx, status);
}

View file

@ -15,29 +15,14 @@
#include "global.h"
#include "video.h"
static void set_player(MenuData *m, void *p) {
#define SELECTED_SUBSHOT(m) ((intptr_t)PLR_SHOT_A + (intptr_t)(m)->context)
static void set_player_mode(MenuData *m, void *p) {
progress.game_settings.character = (CharacterID)(uintptr_t)p;
}
static void set_shotmode(MenuData *m, void *p) {
progress.game_settings.shotmode = (ShotModeID)(uintptr_t)p;
}
static void create_shottype_menu(MenuData *m) {
create_menu(m);
m->transition = NULL;
for(uintptr_t i = 0; i < NUM_SHOT_MODES_PER_CHARACTER; ++i) {
add_menu_entry(m, NULL, set_shotmode, (void*)i);
if(i == progress.game_settings.shotmode) {
m->cursor = i;
}
}
progress.game_settings.shotmode = SELECTED_SUBSHOT(m);
}
static void char_menu_input(MenuData*);
static void free_char_menu(MenuData*);
static void update_char_menu(MenuData *menu) {
for(int i = 0; i < menu->ecount; i++) {
@ -45,28 +30,28 @@ static void update_char_menu(MenuData *menu) {
}
}
void create_char_menu(MenuData *m) {
create_menu(m);
MenuData* create_char_menu(void) {
MenuData *m = alloc_menu();
m->input = char_menu_input;
m->draw = draw_char_menu;
m->logic = update_char_menu;
m->end = free_char_menu;
m->transition = TransMenuDark;
m->flags = MF_Abortable | MF_Transient;
m->context = malloc(sizeof(MenuData));
create_shottype_menu(m->context);
m->flags = MF_Abortable;
m->context = (void*)(intptr_t)progress.game_settings.shotmode;
for(uintptr_t i = 0; i < NUM_CHARACTERS; ++i) {
add_menu_entry(m, NULL, set_player, (void*)i)->transition = TransFadeBlack;
add_menu_entry(m, NULL, set_player_mode, (void*)i)->transition = TransFadeBlack;
if(i == progress.game_settings.character) {
m->cursor = i;
}
}
return m;
}
void draw_char_menu(MenuData *menu) {
MenuData *mod = ((MenuData *)menu->context);
CullFaceMode cull_saved = r_cull_current();
draw_options_menu_bg(menu);
@ -137,11 +122,13 @@ void draw_char_menu(MenuData *menu) {
r_mat_push();
r_mat_translate(SCREEN_W/4*3, SCREEN_H/3, 0);
for(int i = 0; i < mod->ecount; i++) {
PlayerMode *mode = plrmode_find(current_char, (ShotModeID)(uintptr_t)mod->entries[i].arg);
assert(mode != NULL);
ShotModeID current_subshot = SELECTED_SUBSHOT(menu);
if(mod->cursor == i) {
for(ShotModeID shot = PLR_SHOT_A; shot < NUM_SHOT_MODES_PER_CHARACTER; shot++) {
PlayerMode *mode = plrmode_find(current_char, shot);
assume(mode != NULL);
if(shot == current_subshot) {
r_color4(0.9, 0.6, 0.2, 1);
} else {
r_color4(1, 1, 1, 1);
@ -149,7 +136,7 @@ void draw_char_menu(MenuData *menu) {
text_draw(mode->name, &(TextParams) {
.align = ALIGN_CENTER,
.pos = { 0, 200+40*i },
.pos = { 0, 200 + 40 * (shot - PLR_SHOT_A) },
.shader = "text_default",
});
}
@ -181,7 +168,6 @@ void draw_char_menu(MenuData *menu) {
static bool char_menu_input_handler(SDL_Event *event, void *arg) {
MenuData *menu = arg;
MenuData *mod = menu->context;
TaiseiEvent type = TAISEI_EVENT(event->type);
if(type == TE_MENU_CURSOR_RIGHT) {
@ -192,27 +178,24 @@ static bool char_menu_input_handler(SDL_Event *event, void *arg) {
menu->cursor--;
} else if(type == TE_MENU_CURSOR_DOWN) {
play_ui_sound("generic_shot");
mod->cursor++;
menu->context = (void*)(SELECTED_SUBSHOT(menu) + 1);
} else if(type == TE_MENU_CURSOR_UP) {
play_ui_sound("generic_shot");
mod->cursor--;
menu->context = (void*)(SELECTED_SUBSHOT(menu) - 1);
} else if(type == TE_MENU_ACCEPT) {
play_ui_sound("shot_special1");
mod->selected = mod->cursor;
close_menu(mod);
menu->selected = menu->cursor;
close_menu(menu);
// XXX: This needs a better fix
set_shotmode(mod, mod->entries[mod->selected].arg);
} else if(type == TE_MENU_ABORT) {
play_ui_sound("hit");
close_menu(menu);
close_menu(mod);
}
menu->cursor = (menu->cursor % menu->ecount) + menu->ecount*(menu->cursor < 0);
mod->cursor = (mod->cursor % mod->ecount) + mod->ecount*(mod->cursor < 0);
intptr_t ss = SELECTED_SUBSHOT(menu);
ss = (ss % NUM_SHOT_MODES_PER_CHARACTER) + NUM_SHOT_MODES_PER_CHARACTER * (ss < 0);
menu->context = (void*)ss;
return false;
}
@ -223,9 +206,3 @@ static void char_menu_input(MenuData *menu) {
{ NULL }
}, EFLAG_MENU);
}
static void free_char_menu(MenuData *menu) {
MenuData *mod = menu->context;
destroy_menu(mod);
free(mod);
}

View file

@ -13,7 +13,7 @@
#include "menu.h"
void create_char_menu(MenuData *m);
MenuData* create_char_menu(void);
void draw_char_menu(MenuData *menu);
#endif // IGUARD_menu_charselect_h

View file

@ -19,92 +19,159 @@
#include "mainmenu.h"
#include "video.h"
typedef struct StartGameContext {
StageInfo *restart_stage;
StageInfo *current_stage;
MenuData *diff_menu;
MenuData *char_menu;
Difficulty difficulty;
} StartGameContext;
static void start_game_do_pick_character(CallChainResult ccr);
static void start_game_do_enter_stage(CallChainResult ccr);
static void start_game_do_leave_stage(CallChainResult ccr);
static void start_game_do_show_ending(CallChainResult ccr);
static void start_game_do_show_credits(CallChainResult ccr);
static void start_game_do_cleanup(CallChainResult ccr);
static void start_game_internal(MenuData *menu, StageInfo *info, bool difficulty_menu) {
MenuData m;
Difficulty stagediff;
bool restart;
StartGameContext *ctx = calloc(1, sizeof(*ctx));
player_init(&global.plr);
if(info == NULL) {
global.is_practice_mode = false;
ctx->current_stage = stages;
ctx->restart_stage = stages;
} else {
global.is_practice_mode = (info->type != STAGE_EXTRA);
ctx->current_stage = info;
ctx->restart_stage = info;
}
do {
restart = false;
stagediff = info ? info->difficulty : D_Any;
Difficulty stagediff = info ? info->difficulty : D_Any;
if(stagediff == D_Any) {
if(difficulty_menu) {
create_difficulty_menu(&m);
if(menu_loop(&m) == -1) {
return;
}
CallChain cc_pick_character = CALLCHAIN(start_game_do_pick_character, ctx);
global.diff = progress.game_settings.difficulty;
} else {
// assume global.diff is set up beforehand
}
if(stagediff == D_Any) {
if(difficulty_menu) {
ctx->diff_menu = create_difficulty_menu();
enter_menu(ctx->diff_menu, cc_pick_character);
} else {
global.diff = stagediff;
ctx->difficulty = progress.game_settings.difficulty;
run_call_chain(&cc_pick_character, NULL);
}
} else {
ctx->difficulty = stagediff;
run_call_chain(&cc_pick_character, NULL);
}
}
static void start_game_do_pick_character(CallChainResult ccr) {
StartGameContext *ctx = ccr.ctx;
MenuData *prev_menu = ccr.result;
if(prev_menu) {
if(prev_menu->state == MS_Dead) {
start_game_do_cleanup(ccr);
return;
}
create_char_menu(&m);
if(menu_loop(&m) == -1) {
if(stagediff != D_Any || !difficulty_menu) {
return;
}
// came here from the difficulty menu - update our setting
ctx->difficulty = progress.game_settings.difficulty;
}
restart = true;
}
} while(restart);
assert(ctx->char_menu == NULL);
ctx->char_menu = create_char_menu();
enter_menu(ctx->char_menu, CALLCHAIN(start_game_do_enter_stage, ctx));
}
static void reset_game(StartGameContext *ctx) {
ctx->current_stage = ctx->restart_stage;
global.gameover = GAMEOVER_NONE;
global.replay_stage = NULL;
replay_destroy(&global.replay);
replay_init(&global.replay);
player_init(&global.plr);
global.plr.mode = plrmode_find(
progress.game_settings.character,
progress.game_settings.shotmode
);
global.diff = ctx->difficulty;
assert(global.plr.mode != NULL);
}
global.replay_stage = NULL;
replay_init(&global.replay);
PlayerMode *mode = global.plr.mode;
static void kill_aux_menus(StartGameContext *ctx) {
kill_menu(ctx->char_menu);
kill_menu(ctx->diff_menu);
ctx->char_menu = ctx->diff_menu = NULL;
}
do {
restart = false;
static void start_game_do_enter_stage(CallChainResult ccr) {
StartGameContext *ctx = ccr.ctx;
MenuData *prev_menu = ccr.result;
if(info) {
global.is_practice_mode = (info->type != STAGE_EXTRA);
stage_loop(info);
} else {
global.is_practice_mode = false;
for(StageInfo *s = stages; s->type == STAGE_STORY; ++s) {
stage_loop(s);
}
}
if(global.gameover == GAMEOVER_RESTART) {
replay_destroy(&global.replay);
replay_init(&global.replay);
global.gameover = 0;
player_init(&global.plr);
global.plr.mode = mode;
restart = true;
}
} while(restart);
free_resources(false);
ask_save_replay();
global.replay_stage = NULL;
if(global.gameover == GAMEOVER_WIN && !info) {
ending_loop();
credits_loop();
free_resources(false);
if(prev_menu && prev_menu->state == MS_Dead) {
assert(prev_menu == ctx->char_menu);
ctx->char_menu = NULL;
return;
}
start_bgm("menu");
kill_aux_menus(ctx);
reset_game(ctx);
stage_enter(ctx->current_stage, CALLCHAIN(start_game_do_leave_stage, ctx));
}
static void start_game_do_leave_stage(CallChainResult ccr) {
StartGameContext *ctx = ccr.ctx;
if(global.gameover == GAMEOVER_RESTART) {
reset_game(ctx);
stage_enter(ctx->current_stage, CALLCHAIN(start_game_do_leave_stage, ctx));
return;
}
if(ctx->current_stage->type == STAGE_STORY && !global.is_practice_mode) {
++ctx->current_stage;
if(ctx->current_stage->type == STAGE_STORY) {
stage_enter(ctx->current_stage, CALLCHAIN(start_game_do_leave_stage, ctx));
} else {
CallChain cc;
if(global.gameover == GAMEOVER_WIN) {
ending_preload();
credits_preload();
cc = CALLCHAIN(start_game_do_show_ending, ctx);
} else {
cc = CALLCHAIN(start_game_do_cleanup, ctx);
}
ask_save_replay(cc);
}
} else {
ask_save_replay(CALLCHAIN(start_game_do_cleanup, ctx));
}
}
static void start_game_do_show_ending(CallChainResult ccr) {
ending_enter(CALLCHAIN(start_game_do_show_credits, ccr.ctx));
}
static void start_game_do_show_credits(CallChainResult ccr) {
credits_enter(CALLCHAIN(start_game_do_cleanup, ccr.ctx));
}
static void start_game_do_cleanup(CallChainResult ccr) {
StartGameContext *ctx = ccr.ctx;
kill_aux_menus(ctx);
free(ctx);
free_resources(false);
global.replay_stage = NULL;
global.gameover = GAMEOVER_NONE;
replay_destroy(&global.replay);
main_menu_update_practice_menus();
global.gameover = 0;
start_bgm("menu");
}
void start_game(MenuData *m, void *arg) {
@ -126,7 +193,7 @@ void draw_menu_selector(float x, float y, float w, float h, float t) {
r_mat_pop();
}
void draw_menu_title(MenuData *m, char *title) {
void draw_menu_title(MenuData *m, const char *title) {
text_draw(title, &(TextParams) {
.pos = { (text_width(get_font("big"), title, 0) + 10) * (1.0 - menu_fade(m)), 30 },
.align = ALIGN_RIGHT,
@ -200,6 +267,6 @@ void animate_menu_list(MenuData *m) {
animate_menu_list_entries(m);
}
void menu_commonaction_close(MenuData *menu, void *arg) {
void menu_action_close(MenuData *menu, void *arg) {
menu->state = MS_Dead;
}

View file

@ -16,11 +16,11 @@
void start_game(MenuData *m, void *arg);
void start_game_no_difficulty_menu(MenuData *m, void *arg);
void draw_menu_selector(float x, float y, float w, float h, float t);
void draw_menu_title(MenuData *m, char *title);
void draw_menu_title(MenuData *m, const char *title);
void draw_menu_list(MenuData *m, float x, float y, void (*draw)(void*, int, int));
void animate_menu_list(MenuData *m);
void animate_menu_list_entries(MenuData *m);
void animate_menu_list_entry(MenuData *m, int i);
void menu_commonaction_close(MenuData *menu, void *arg);
void menu_action_close(MenuData *menu, void *arg);
#endif // IGUARD_menu_common_h

View file

@ -32,12 +32,13 @@ static void update_difficulty_menu(MenuData *menu) {
color_approach(&diff_color, difficulty_color(menu->cursor + D_Easy), 0.1);
}
void create_difficulty_menu(MenuData *m) {
create_menu(m);
MenuData* create_difficulty_menu(void) {
MenuData *m = alloc_menu();
m->draw = draw_difficulty_menu;
m->logic = update_difficulty_menu;
m->transition = TransMenuDark;
m->flags = MF_Transient | MF_Abortable;
m->flags = MF_Abortable;
add_menu_entry(m, "“All those bullets confuse me!”\nYou will be stuck here forever", set_difficulty, (void *)D_Easy);
add_menu_entry(m, "“So it's not just about shooting stuff?”\nSomewhat challenging", set_difficulty, (void *)D_Normal);
@ -52,6 +53,8 @@ void create_difficulty_menu(MenuData *m) {
break;
}
}
return m;
}
void draw_difficulty_menu(MenuData *menu) {

View file

@ -13,7 +13,7 @@
#include "menu.h"
void create_difficulty_menu(MenuData *menu);
MenuData* create_difficulty_menu(void);
void draw_difficulty_menu(MenuData *m);
#endif // IGUARD_menu_difficultyselect_h

View file

@ -23,8 +23,9 @@ static void give_up(MenuData *m, void *arg) {
global.gameover = (MAX_CONTINUES - global.plr.continues_used)? GAMEOVER_ABORT : GAMEOVER_DEFEAT;
}
void create_gameover_menu(MenuData *m) {
create_menu(m);
MenuData* create_gameover_menu(void) {
MenuData *m = alloc_menu();
m->draw = draw_ingame_menu;
m->logic = update_ingame_menu;
m->flags = MF_Transient | MF_AlwaysProcessInput;
@ -50,4 +51,5 @@ void create_gameover_menu(MenuData *m) {
}
set_transition(TransEmpty, 0, m->transition_out_time);
return m;
}

View file

@ -13,6 +13,6 @@
#include "menu.h"
void create_gameover_menu(MenuData *);
MenuData* create_gameover_menu(void);
#endif // IGUARD_menu_gameovermenu_h

View file

@ -20,12 +20,12 @@
static void return_to_title(MenuData *m, void *arg) {
global.gameover = GAMEOVER_ABORT;
menu_commonaction_close(m, arg);
menu_action_close(m, arg);
}
void restart_game(MenuData *m, void *arg) {
global.gameover = GAMEOVER_RESTART;
menu_commonaction_close(m, arg);
menu_action_close(m, arg);
}
static void ingame_menu_do(MenuData *m, MenuAction action) {
@ -113,8 +113,9 @@ static void ingame_menu_input(MenuData *m) {
}, EFLAG_MENU);
}
void create_ingame_menu(MenuData *m) {
create_menu(m);
MenuData* create_ingame_menu(void) {
MenuData *m = alloc_menu();
m->draw = draw_ingame_menu;
m->logic = update_ingame_menu;
m->input = ingame_menu_input;
@ -122,33 +123,38 @@ void create_ingame_menu(MenuData *m) {
m->transition = TransEmpty;
m->cursor = 1;
m->context = "Game Paused";
add_menu_entry(m, "Options", enter_options, NULL)->transition = TransMenuDark;
add_menu_entry(m, "Return to Game", menu_commonaction_close, NULL);
add_menu_entry(m, "Options", menu_action_enter_options, NULL)->transition = TransMenuDark;
add_menu_entry(m, "Return to Game", menu_action_close, NULL);
add_menu_entry(m, "Restart the Game", restart_game, NULL)->transition = TransFadeBlack;
add_menu_entry(m, "Stop the Game", return_to_title, NULL)->transition = TransFadeBlack;
set_transition(TransEmpty, 0, m->transition_out_time);
return m;
}
static void skip_stage(MenuData *m, void *arg) {
global.gameover = GAMEOVER_WIN;
menu_commonaction_close(m, arg);
menu_action_close(m, arg);
}
void create_ingame_menu_replay(MenuData *m) {
create_menu(m);
MenuData* create_ingame_menu_replay(void) {
MenuData *m = alloc_menu();
m->draw = draw_ingame_menu;
m->logic = update_ingame_menu;
m->flags = MF_Abortable | MF_AlwaysProcessInput;
m->transition = TransEmpty;
m->cursor = 1;
m->context = "Replay Paused";
add_menu_entry(m, "Options", enter_options, NULL)->transition = TransMenuDark;
add_menu_entry(m, "Continue Watching", menu_commonaction_close, NULL);
add_menu_entry(m, "Options", menu_action_enter_options, NULL)->transition = TransMenuDark;
add_menu_entry(m, "Continue Watching", menu_action_close, NULL);
add_menu_entry(m, "Restart the Stage", restart_game, NULL)->transition = TransFadeBlack;
add_menu_entry(m, "Skip the Stage", skip_stage, NULL)->transition = TransFadeBlack;
add_menu_entry(m, "Stop Watching", return_to_title, NULL)->transition = TransFadeBlack;
m->cursor = 1;
set_transition(TransEmpty, 0, m->transition_out_time);
return m;
}
void draw_ingame_menu_bg(MenuData *menu, float f) {

View file

@ -16,8 +16,8 @@
void draw_ingame_menu_bg(MenuData *menu, float f);
void draw_ingame_menu(MenuData *menu);
void create_ingame_menu(MenuData *menu);
void create_ingame_menu_replay(MenuData *m);
MenuData* create_ingame_menu(void);
MenuData* create_ingame_menu_replay(void);
void update_ingame_menu(MenuData *menu);

View file

@ -36,14 +36,14 @@ void main_menu_update_practice_menus(void) {
StageProgress *p = stage_get_progress_from_info(stg, D_Any, false);
if(p && p->unlocked) {
spell_practice_entry->action = enter_spellpractice;
spell_practice_entry->action = menu_action_enter_spellpractice;
}
} else if(stg->type == STAGE_STORY) {
for(Difficulty d = D_Easy; d <= D_Lunatic; ++d) {
StageProgress *p = stage_get_progress_from_info(stg, d, false);
if(p && p->unlocked) {
stage_practice_entry->action = enter_stagepractice;
stage_practice_entry->action = menu_action_enter_stagepractice;
}
}
}
@ -63,6 +63,7 @@ static void update_main_menu(MenuData *menu) {
}
}
attr_unused
static bool main_menu_input_handler(SDL_Event *event, void *arg) {
MenuData *m = arg;
TaiseiEvent te = TAISEI_EVENT(event->type);
@ -85,6 +86,7 @@ static bool main_menu_input_handler(SDL_Event *event, void *arg) {
return menu_input_handler(event, arg);
}
attr_unused
static void main_menu_input(MenuData *m) {
events_poll((EventHandler[]){
{ .proc = main_menu_input_handler, .arg = m },
@ -92,27 +94,34 @@ static void main_menu_input(MenuData *m) {
}, EFLAG_MENU);
}
void create_main_menu(MenuData *m) {
create_menu(m);
MenuData* create_main_menu(void) {
MenuData *m = alloc_menu();
m->begin = begin_main_menu;
m->draw = draw_main_menu;
m->logic = update_main_menu;
m->begin = begin_main_menu;
m->input = main_menu_input;
add_menu_entry(m, "Start Story", start_game, NULL);
add_menu_entry(m, "Start Extra", NULL, NULL);
add_menu_entry(m, "Stage Practice", enter_stagepractice, NULL);
add_menu_entry(m, "Spell Practice", enter_spellpractice, NULL);
add_menu_entry(m, "Stage Practice", menu_action_enter_stagepractice, NULL);
add_menu_entry(m, "Spell Practice", menu_action_enter_spellpractice, NULL);
#ifdef DEBUG
add_menu_entry(m, "Select Stage", enter_stagemenu, NULL);
add_menu_entry(m, "Select Stage", menu_action_enter_stagemenu, NULL);
#endif
add_menu_entry(m, "Replays", menu_action_enter_replayview, NULL);
add_menu_entry(m, "Options", menu_action_enter_options, NULL);
#ifndef __EMSCRIPTEN__
add_menu_entry(m, "Quit", menu_action_close, NULL)->transition = TransFadeBlack;
m->input = main_menu_input;
#endif
add_menu_entry(m, "Replays", enter_replayview, NULL);
add_menu_entry(m, "Options", enter_options, NULL);
add_menu_entry(m, "Quit", menu_commonaction_close, NULL)->transition = TransFadeBlack;
stage_practice_entry = m->entries + 2;
spell_practice_entry = m->entries + 3;
main_menu_update_practice_menus();
start_bgm("menu");
return m;
}
void draw_main_menu_bg(MenuData* menu) {

View file

@ -13,7 +13,7 @@
#include "menu.h"
void create_main_menu(MenuData *m);
MenuData* create_main_menu(void);
void draw_main_menu_bg(MenuData *m);
void draw_main_menu(MenuData *m);
void main_menu_update_practice_menus(void);

View file

@ -30,26 +30,42 @@ void add_menu_separator(MenuData *menu) {
memset(menu->entries + menu->ecount - 1, 0, sizeof(MenuEntry));
}
void destroy_menu(MenuData *menu) {
void free_menu(MenuData *menu) {
if(menu == NULL) {
return;
}
for(int i = 0; i < menu->ecount; i++) {
free(menu->entries[i].name);
}
free(menu->entries);
free(menu);
}
void create_menu(MenuData *menu) {
memset(menu, 0, sizeof(MenuData));
MenuData* alloc_menu(void) {
MenuData *menu = calloc(1, sizeof(*menu));
menu->selected = -1;
menu->transition = TransMenu; // TransFadeBlack;
menu->transition_in_time = FADE_TIME;
menu->transition_out_time = FADE_TIME;
menu->fade = 1.0;
menu->input = menu_input;
return menu;
}
void kill_menu(MenuData *menu) {
if(menu != NULL) {
menu->state = MS_Dead;
// nani?!
}
}
static void close_menu_finish(MenuData *menu) {
// This may happen with MF_AlwaysProcessInput menus, so make absolutely sure we
// never run the call chain with menu->state == MS_Dead more than once.
bool was_dead = (menu->state == MS_Dead);
menu->state = MS_Dead;
if(menu->selected != -1 && menu->entries[menu->selected].action != NULL) {
@ -59,6 +75,10 @@ static void close_menu_finish(MenuData *menu) {
menu->entries[menu->selected].action(menu, menu->entries[menu->selected].arg);
}
if(!was_dead) {
run_call_chain(&menu->cc, menu);
}
}
void close_menu(MenuData *menu) {
@ -144,9 +164,13 @@ void menu_no_input(MenuData *menu) {
events_poll(NULL, 0);
}
static FrameAction menu_logic_frame(void *arg) {
static LogicFrameAction menu_logic_frame(void *arg) {
MenuData *menu = arg;
if(menu->state == MS_Dead) {
return LFRAME_STOP;
}
if(menu->logic) {
menu->logic(menu);
}
@ -162,30 +186,45 @@ static FrameAction menu_logic_frame(void *arg) {
update_transition();
return menu->state == MS_Dead ? LFRAME_STOP : LFRAME_WAIT;
return LFRAME_WAIT;
}
static FrameAction menu_render_frame(void *arg) {
static RenderFrameAction menu_render_frame(void *arg) {
MenuData *menu = arg;
assert(menu->draw);
set_ortho(SCREEN_W, SCREEN_H);
menu->draw(menu);
draw_transition();
return RFRAME_SWAP;
}
int menu_loop(MenuData *menu) {
set_ortho(SCREEN_W, SCREEN_H);
static void menu_end_loop(void *ctx) {
MenuData *menu = ctx;
if(menu->begin) {
menu->begin(menu);
if(menu->state != MS_Dead) {
// definitely dead now...
menu->state = MS_Dead;
run_call_chain(&menu->cc, menu);
}
loop_at_fps(menu_logic_frame, menu_render_frame, menu, FPS);
if(menu->end) {
menu->end(menu);
}
destroy_menu(menu);
return menu->selected;
free_menu(menu);
}
void enter_menu(MenuData *menu, CallChain next) {
if(menu == NULL) {
run_call_chain(&next, NULL);
return;
}
menu->cc = next;
if(menu->begin != NULL) {
menu->begin(menu);
}
eventloop_enter(menu, menu_logic_frame, menu_render_frame, menu_end_loop, FPS);
}

View file

@ -12,11 +12,11 @@
#include "taisei.h"
#include "transition.h"
#include "events.h"
#include "eventloop/eventloop.h"
#define IMENU_BLUR 0.05
#include "events.h"
enum {
FADE_TIME = 15
};
@ -69,29 +69,33 @@ struct MenuData {
void *context;
MenuProc begin;
MenuProc draw;
MenuProc input;
MenuProc logic;
MenuProc begin;
MenuProc end;
CallChain cc;
};
MenuData *alloc_menu(void);
void free_menu(MenuData *menu);
void close_menu(MenuData *menu);
void kill_menu(MenuData *menu);
// You will get a pointer to the MenuData in callchain result.
// WARNING: Unless the menu state is MS_Dead at the time your callback is invoked, it may be called again!
// This must be accounted for, otherwise demons will literally fly out of your nose.
// No joke. Not *might*, they actually *will*. I wouldn't recommend testing that out.
void enter_menu(MenuData *menu, CallChain next);
MenuEntry *add_menu_entry(MenuData *menu, const char *name, MenuAction action, void *arg);
MenuEntry *add_menu_entry_f(MenuData *menu, char *name, MenuAction action, void *arg, int flags);
void add_menu_separator(MenuData *menu);
void create_menu(MenuData *menu);
void destroy_menu(MenuData *menu);
void menu_input(MenuData *menu);
void menu_no_input(MenuData *menu);
void close_menu(MenuData *menu);
void menu_key_action(MenuData *menu, int sym);
int menu_loop(MenuData *menu);
float menu_fade(MenuData *menu);
bool menu_input_handler(SDL_Event *event, void *arg);

View file

@ -15,6 +15,45 @@
#include "global.h"
#include "video.h"
typedef struct OptionBinding OptionBinding;
typedef int (*BindingGetter)(OptionBinding*);
typedef int (*BindingSetter)(OptionBinding*, int);
typedef bool (*BindingDependence)(void);
typedef enum BindingType {
BT_IntValue,
BT_KeyBinding,
BT_StrValue,
BT_Resolution,
BT_Scale,
BT_GamepadKeyBinding,
BT_GamepadAxisBinding,
BT_GamepadDevice,
} BindingType;
typedef struct OptionBinding {
union {
char **values;
char *strvalue;
};
bool displaysingle;
int valcount;
int valrange_min;
int valrange_max;
float scale_min;
float scale_max;
float scale_step;
BindingGetter getter;
BindingSetter setter;
BindingDependence dependence;
int selected;
int configentry;
BindingType type;
bool blockinput;
int pad;
} OptionBinding;
// --- Menu entry <-> config option binding stuff --- //
static void bind_init(OptionBinding *bind) {
@ -241,10 +280,6 @@ static int bind_setprev(OptionBinding *b) {
return bind_setvalue(b, s);
}
static void bind_setdependence(OptionBinding *b, BindingDependence dep) {
b->dependence = dep;
}
static bool bind_isactive(OptionBinding *b) {
if(!b->dependence)
return true;
@ -298,7 +333,7 @@ static int bind_common_intplus1_set(OptionBinding *b, int v) {
// --- Binding callbacks for individual options --- //
static bool bind_resizable_dependence(void) {
return !config_get_int(CONFIG_FULLSCREEN);
return video_query_capability(VIDEO_CAP_EXTERNAL_RESIZE) == VIDEO_AVAILABLE;
}
static bool bind_bgquality_dependence(void) {
@ -306,7 +341,11 @@ static bool bind_bgquality_dependence(void) {
}
static bool bind_resolution_dependence(void) {
return video_can_change_resolution();
return video_query_capability(VIDEO_CAP_CHANGE_RESOLUTION) == VIDEO_AVAILABLE;
}
static bool bind_fullscreen_dependence(void) {
return video_query_capability(VIDEO_CAP_FULLSCREEN) == VIDEO_AVAILABLE;
}
static int bind_resolution_set(OptionBinding *b, int v) {
@ -329,6 +368,11 @@ static int bind_power_get(OptionBinding *b) {
// --- Creating, destroying, filling the menu --- //
typedef struct OptionsMenuContext {
const char *title;
void *data;
} OptionsMenuContext;
static void destroy_options_menu(MenuData *m) {
for(int i = 0; i < m->ecount; ++i) {
OptionBinding *bind = bind_get(m, i);
@ -337,7 +381,7 @@ static void destroy_options_menu(MenuData *m) {
continue;
}
if(bind->type == BT_Resolution && video_can_change_resolution()) {
if(bind->type == BT_Resolution && video_query_capability(VIDEO_CAP_CHANGE_RESOLUTION) == VIDEO_AVAILABLE) {
if(bind->selected != -1) {
VideoMode *mode = video.modes + bind->selected;
@ -354,35 +398,67 @@ static void destroy_options_menu(MenuData *m) {
bind_free(bind);
free(bind);
}
if(m->context) {
OptionsMenuContext *ctx = m->context;
free(ctx->data);
free(ctx);
}
}
static void do_nothing(MenuData *menu, void *arg) { }
static void update_options_menu(MenuData *menu);
void options_menu_input(MenuData*);
static void options_menu_input(MenuData*);
static void draw_options_menu(MenuData*);
static void create_options_menu_basic(MenuData *m, char *s) {
create_menu(m);
#define bind_onoff(b) bind_addvalue(b, "on"); bind_addvalue(b, "off")
static MenuData* create_options_menu_base(const char *s) {
MenuData *m = alloc_menu();
m->transition = TransMenuDark;
m->flags = MF_Abortable;
m->context = s;
m->input = options_menu_input;
m->draw = draw_options_menu;
m->logic = update_options_menu;
m->end = destroy_options_menu;
OptionsMenuContext *ctx = calloc(1, sizeof(OptionsMenuContext));
ctx->title = s;
m->context = ctx;
return m;
}
#define bind_onoff(b) bind_addvalue(b, "on"); bind_addvalue(b, "off")
static void options_enter_sub(MenuData *parent, MenuData *(*construct)(MenuData*)) {
parent->frames = 0;
enter_menu(construct(parent), NO_CALLCHAIN);
}
static void options_sub_video(MenuData *parent, void *arg) {
MenuData menu, *m;
#define DECLARE_ENTER_FUNC(enter, construct) \
static void enter(MenuData *parent, void *arg) { \
options_enter_sub(parent, construct); \
}
static MenuData* create_options_menu_controls(MenuData *parent);
DECLARE_ENTER_FUNC(enter_options_menu_controls, create_options_menu_controls)
static MenuData* create_options_menu_gamepad(MenuData *parent);
DECLARE_ENTER_FUNC(enter_options_menu_gamepad, create_options_menu_gamepad)
static MenuData* create_options_menu_gamepad_controls(MenuData *parent);
DECLARE_ENTER_FUNC(enter_options_menu_gamepad_controls, create_options_menu_gamepad_controls)
static MenuData* create_options_menu_video(MenuData *parent);
DECLARE_ENTER_FUNC(enter_options_menu_video, create_options_menu_video)
static MenuData* create_options_menu_video(MenuData *parent) {
MenuData *m = create_options_menu_base("Video Options");
OptionBinding *b;
m = &menu;
create_options_menu_basic(m, "Video Options");
add_menu_entry(m, "Fullscreen", do_nothing,
b = bind_option(CONFIG_FULLSCREEN, bind_common_onoff_get, bind_common_onoff_set)
); bind_onoff(b);
b->dependence = bind_fullscreen_dependence;
add_menu_entry(m, "Window size", do_nothing,
b = bind_resolution()
@ -392,7 +468,7 @@ static void options_sub_video(MenuData *parent, void *arg) {
add_menu_entry(m, "Resizable window", do_nothing,
b = bind_option(CONFIG_VID_RESIZABLE, bind_common_onoff_get, bind_common_onoff_set)
); bind_onoff(b);
bind_setdependence(b, bind_resizable_dependence);
b->dependence = bind_resizable_dependence;
add_menu_entry(m, "Pause the game when it's not focused", do_nothing,
b = bind_option(CONFIG_FOCUS_LOSS_PAUSE, bind_common_onoff_get, bind_common_onoff_set)
@ -404,7 +480,10 @@ static void options_sub_video(MenuData *parent, void *arg) {
b = bind_option(CONFIG_VSYNC, bind_common_onoffplus_get, bind_common_onoffplus_set)
); bind_addvalue(b, "on");
bind_addvalue(b, "off");
if(video_query_capability(VIDEO_CAP_VSYNC_ADAPTIVE) != VIDEO_NEVER_AVAILABLE) {
bind_addvalue(b, "adaptive");
}
add_menu_entry(m, "Skip frames", do_nothing,
b = bind_option(CONFIG_VID_FRAMESKIP, bind_common_intplus1_get, bind_common_intplus1_set)
@ -446,10 +525,9 @@ static void options_sub_video(MenuData *parent, void *arg) {
bind_addvalue(b, "full");
add_menu_separator(m);
add_menu_entry(m, "Back", menu_commonaction_close, NULL);
add_menu_entry(m, "Back", menu_action_close, NULL);
menu_loop(m);
parent->frames = 0;
return m;
}
attr_unused
@ -465,11 +543,8 @@ static bool gamepad_enabled_depencence(void) {
return config_get_int(CONFIG_GAMEPAD_ENABLED);
}
static void options_sub_gamepad_controls(MenuData *parent, void *arg) {
MenuData menu, *m;
m = &menu;
create_options_menu_basic(m, "Gamepad Controls");
static MenuData* create_options_menu_gamepad_controls(MenuData *parent) {
MenuData *m = create_options_menu_base("Gamepad Controls");
add_menu_entry(m, "Move up", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_UP)
@ -512,18 +587,29 @@ static void options_sub_gamepad_controls(MenuData *parent, void *arg) {
);
add_menu_separator(m);
add_menu_entry(m, "Back", menu_commonaction_close, NULL);
add_menu_entry(m, "Back", menu_action_close, NULL);
menu_loop(m);
parent->frames = 0;
return m;
}
static void options_sub_gamepad(MenuData *parent, void *arg) {
MenuData menu, *m;
OptionBinding *b;
m = &menu;
static void destroy_options_menu_gamepad(MenuData *m) {
OptionsMenuContext *ctx = m->context;
create_options_menu_basic(m, "Gamepad Options");
if(config_get_int(CONFIG_GAMEPAD_ENABLED) && strcasecmp(config_get_str(CONFIG_GAMEPAD_DEVICE), ctx->data)) {
gamepad_restart();
}
destroy_options_menu(m);
}
static MenuData* create_options_menu_gamepad(MenuData *parent) {
MenuData *m = create_options_menu_base("Gamepad Options");
m->end = destroy_options_menu_gamepad;
OptionsMenuContext *ctx = m->context;
ctx->data = strdup(config_get_str(CONFIG_GAMEPAD_DEVICE));
OptionBinding *b;
add_menu_entry(m, "Enable Gamepad/Joystick support", do_nothing,
b = bind_option(CONFIG_GAMEPAD_ENABLED, bind_common_onoff_get, bind_common_onoff_set)
@ -531,10 +617,10 @@ static void options_sub_gamepad(MenuData *parent, void *arg) {
add_menu_entry(m, "Device", do_nothing,
b = bind_gpdevice(CONFIG_GAMEPAD_DEVICE)
); bind_setdependence(b, gamepad_enabled_depencence);
); b->dependence = gamepad_enabled_depencence;
add_menu_separator(m);
add_menu_entry(m, "Customize controls…", options_sub_gamepad_controls, NULL);
add_menu_entry(m, "Customize controls…", enter_options_menu_gamepad_controls, NULL);
add_menu_separator(m);
@ -564,25 +650,13 @@ static void options_sub_gamepad(MenuData *parent, void *arg) {
);
add_menu_separator(m);
add_menu_entry(m, "Back", menu_commonaction_close, NULL);
add_menu_entry(m, "Back", menu_action_close, NULL);
char *gpdev = strdup(config_get_str(CONFIG_GAMEPAD_DEVICE));
menu_loop(m);
parent->frames = 0;
if(config_get_int(CONFIG_GAMEPAD_ENABLED) && strcasecmp(config_get_str(CONFIG_GAMEPAD_DEVICE), gpdev)) {
gamepad_restart();
}
free(gpdev);
return m;
}
static void options_sub_controls(MenuData *parent, void *arg) {
MenuData menu, *m;
m = &menu;
create_options_menu_basic(m, "Controls");
static MenuData* create_options_menu_controls(MenuData *parent) {
MenuData *m = create_options_menu_base("Controls");
add_menu_entry(m, "Move up", do_nothing,
bind_keybinding(CONFIG_KEY_UP)
@ -677,17 +751,15 @@ static void options_sub_controls(MenuData *parent, void *arg) {
#endif
add_menu_separator(m);
add_menu_entry(m, "Back", menu_commonaction_close, NULL);
add_menu_entry(m, "Back", menu_action_close, NULL);
menu_loop(m);
parent->frames = 0;
return m;
}
void create_options_menu(MenuData *m) {
MenuData* create_options_menu(void) {
MenuData *m = create_options_menu_base("Options");
OptionBinding *b;
create_options_menu_basic(m, "Options");
add_menu_entry(m, "Player name", do_nothing,
b = bind_stroption(CONFIG_PLAYERNAME)
);
@ -725,23 +797,25 @@ void create_options_menu(MenuData *m) {
add_menu_entry(m, "SFX Volume", do_nothing,
b = bind_scale(CONFIG_SFX_VOLUME, 0, 1, 0.1)
); bind_setdependence(b, audio_output_works);
); b->dependence = audio_output_works;
add_menu_entry(m, "BGM Volume", do_nothing,
b = bind_scale(CONFIG_BGM_VOLUME, 0, 1, 0.1)
); bind_setdependence(b, audio_output_works);
); b->dependence = audio_output_works;
add_menu_entry(m, "Mute audio", do_nothing,
b = bind_option(CONFIG_MUTE_AUDIO, bind_common_onoff_get, bind_common_onoff_set)
); bind_onoff(b);
add_menu_separator(m);
add_menu_entry(m, "Video options…", options_sub_video, NULL);
add_menu_entry(m, "Customize controls…", options_sub_controls, NULL);
add_menu_entry(m, "Gamepad & Joystick options…", options_sub_gamepad, NULL);
add_menu_entry(m, "Video options…", enter_options_menu_video, NULL);
add_menu_entry(m, "Customize controls…", enter_options_menu_controls, NULL);
add_menu_entry(m, "Gamepad & Joystick options…", enter_options_menu_gamepad, NULL);
add_menu_separator(m);
add_menu_entry(m, "Back", menu_commonaction_close, NULL);
add_menu_entry(m, "Back", menu_action_close, NULL);
return m;
}
// --- Drawing the menu --- //
@ -766,9 +840,11 @@ static void update_options_menu(MenuData *menu) {
}
}
void draw_options_menu(MenuData *menu) {
static void draw_options_menu(MenuData *menu) {
OptionsMenuContext *ctx = menu->context;
draw_options_menu_bg(menu);
draw_menu_title(menu, menu->context);
draw_menu_title(menu, ctx->title);
r_mat_push();
r_mat_translate(100, 100, 0);
@ -1350,7 +1426,7 @@ static bool options_input_handler(SDL_Event *event, void *arg) {
}
#undef SHOULD_SKIP
void options_menu_input(MenuData *menu) {
static void options_menu_input(MenuData *menu) {
OptionBinding *b;
EventFlags flags = EFLAG_MENU;

View file

@ -13,48 +13,7 @@
#include "menu.h"
void create_options_menu(MenuData *m);
void draw_options_menu(MenuData *m);
typedef struct OptionBinding OptionBinding;
typedef int (*BindingGetter)(OptionBinding*);
typedef int (*BindingSetter)(OptionBinding*, int);
typedef bool (*BindingDependence)(void);
typedef enum BindingType {
BT_IntValue,
BT_KeyBinding,
BT_StrValue,
BT_Resolution,
BT_Scale,
BT_GamepadKeyBinding,
BT_GamepadAxisBinding,
BT_GamepadDevice,
} BindingType;
typedef struct OptionBinding {
union {
char **values;
char *strvalue;
};
bool displaysingle;
int valcount;
int valrange_min;
int valrange_max;
float scale_min;
float scale_max;
float scale_step;
BindingGetter getter;
BindingSetter setter;
BindingDependence dependence;
int selected;
int configentry;
BindingType type;
bool blockinput;
int pad;
} OptionBinding;
MenuData* create_options_menu(void);
void draw_options_menu_bg(MenuData*);
#endif // IGUARD_menu_options_h

View file

@ -54,14 +54,16 @@ typedef struct {
int stgnum;
} startrpy_arg_t;
static void really_start_replay(void *varg) {
startrpy_arg_t arg;
memcpy(&arg, varg, sizeof(arg));
free(varg);
replay_play(arg.rpy, arg.stgnum);
static void on_replay_finished(CallChainResult ccr) {
start_bgm("menu");
}
static void really_start_replay(void *varg) {
startrpy_arg_t arg = *(startrpy_arg_t*)varg;
free(varg);
replay_play(arg.rpy, arg.stgnum, CALLCHAIN(on_replay_finished, NULL));
}
static void start_replay(MenuData *menu, void *arg) {
ReplayviewItemContext *ictx = arg;
ReplayviewContext *mctx = menu->context;
@ -109,10 +111,9 @@ static void replayview_draw_stagemenu(MenuData*);
static void replayview_draw_messagebox(MenuData*);
static MenuData* replayview_sub_stageselect(MenuData *parent, ReplayviewItemContext *ictx) {
MenuData *m = malloc(sizeof(MenuData));
MenuData *m = alloc_menu();
Replay *rpy = ictx->replay;
create_menu(m);
m->draw = replayview_draw_stagemenu;
m->flags = MF_Transient | MF_Abortable;
m->transition = NULL;
@ -126,13 +127,12 @@ static MenuData* replayview_sub_stageselect(MenuData *parent, ReplayviewItemCont
}
static MenuData* replayview_sub_messagebox(MenuData *parent, const char *message) {
MenuData *m = malloc(sizeof(MenuData));
create_menu(m);
MenuData *m = alloc_menu();
m->draw = replayview_draw_messagebox;
m->flags = MF_Transient | MF_Abortable;
m->transition = NULL;
m->context = parent->context;
add_menu_entry(m, message, menu_commonaction_close, NULL);
add_menu_entry(m, message, menu_action_close, NULL);
return m;
}
@ -321,8 +321,7 @@ static void replayview_logic(MenuData *m) {
if(sm->state == MS_Dead) {
if(ctx->sub_fade == 1.0) {
destroy_menu(sm);
free(sm);
free_menu(sm);
ctx->submenu = ctx->next_submenu;
ctx->next_submenu = NULL;
return;
@ -427,16 +426,8 @@ static void replayview_free(MenuData *m) {
if(m->context) {
ReplayviewContext *ctx = m->context;
if(ctx->submenu) {
destroy_menu(ctx->submenu);
free(ctx->submenu);
}
if(ctx->next_submenu) {
destroy_menu(ctx->next_submenu);
free(ctx->next_submenu);
}
free_menu(ctx->next_submenu);
free_menu(ctx->submenu);
free(m->context);
m->context = NULL;
}
@ -448,8 +439,9 @@ static void replayview_free(MenuData *m) {
}
}
void create_replayview_menu(MenuData *m) {
create_menu(m);
MenuData* create_replayview_menu(void) {
MenuData *m = alloc_menu();
m->logic = replayview_logic;
m->input = replayview_menu_input;
m->draw = replayview_draw;
@ -466,11 +458,13 @@ void create_replayview_menu(MenuData *m) {
int r = fill_replayview_menu(m);
if(!r) {
add_menu_entry(m, "No replays available. Play the game and record some!", menu_commonaction_close, NULL);
add_menu_entry(m, "No replays available. Play the game and record some!", menu_action_close, NULL);
} else if(r < 0) {
add_menu_entry(m, "There was a problem getting the replay list :(", menu_commonaction_close, NULL);
add_menu_entry(m, "There was a problem getting the replay list :(", menu_action_close, NULL);
} else {
add_menu_separator(m);
add_menu_entry(m, "Back", menu_commonaction_close, NULL);
add_menu_entry(m, "Back", menu_action_close, NULL);
}
return m;
}

View file

@ -13,6 +13,6 @@
#include "menu.h"
void create_replayview_menu(MenuData *m);
MenuData* create_replayview_menu(void);
#endif // IGUARD_menu_replayview_h

View file

@ -111,35 +111,33 @@ static void update_saverpy_menu(MenuData *m) {
}
}
void create_saverpy_menu(MenuData *m) {
create_menu(m);
static MenuData* create_saverpy_menu(void) {
MenuData *m = alloc_menu();
m->input = saverpy_menu_input;
m->draw = draw_saverpy_menu;
m->logic = update_saverpy_menu;
m->flags = MF_Transient;
add_menu_entry(m, "Yes", save_rpy, NULL);
add_menu_entry(m, "No", menu_commonaction_close, NULL);
add_menu_entry(m, "No", menu_action_close, NULL);
return m;
}
void ask_save_replay(void) {
void ask_save_replay(CallChain next) {
assert(global.replay_stage != NULL);
switch(config_get_int(CONFIG_SAVE_RPY)) {
case 0: {
break;
}
case 1: {
case 1:
do_save_replay(&global.replay);
// fallthrough
case 0:
run_call_chain(&next, NULL);
break;
}
case 2: {
MenuData m;
create_saverpy_menu(&m);
menu_loop(&m);
case 2:
enter_menu(create_saverpy_menu(), next);
break;
}
}
}

View file

@ -12,8 +12,8 @@
#include "taisei.h"
#include "menu.h"
#include "eventloop/eventloop.h"
void create_saverpy_menu(MenuData*);
void ask_save_replay(void);
void ask_save_replay(CallChain next);
#endif // IGUARD_menu_savereplay_h

View file

@ -19,11 +19,12 @@ static void draw_spell_menu(MenuData *m) {
draw_menu_list(m, 100, 100, NULL);
}
void create_spell_menu(MenuData *m) {
MenuData* create_spell_menu(void) {
char title[128];
Difficulty lastdiff = D_Any;
create_menu(m);
MenuData *m = alloc_menu();
m->draw = draw_spell_menu;
m->logic = animate_menu_list;
m->flags = MF_Abortable;
@ -51,9 +52,11 @@ void create_spell_menu(MenuData *m) {
}
add_menu_separator(m);
add_menu_entry(m, "Back", menu_commonaction_close, NULL);
add_menu_entry(m, "Back", menu_action_close, NULL);
while(!m->entries[m->cursor].action) {
++m->cursor;
}
return m;
}

View file

@ -13,6 +13,6 @@
#include "menu.h"
void create_spell_menu(MenuData *m);
MenuData* create_spell_menu(void);
#endif // IGUARD_menu_spellpractice_h

View file

@ -19,10 +19,11 @@ static void draw_stgpract_menu(MenuData *m) {
draw_menu_list(m, 100, 100, NULL);
}
void create_stgpract_menu(MenuData *m, Difficulty diff) {
MenuData* create_stgpract_menu(Difficulty diff) {
char title[128];
create_menu(m);
MenuData *m = alloc_menu();
m->draw = draw_stgpract_menu;
m->logic = animate_menu_list;
m->flags = MF_Abortable;
@ -45,9 +46,11 @@ void create_stgpract_menu(MenuData *m, Difficulty diff) {
}
add_menu_separator(m);
add_menu_entry(m, "Back", menu_commonaction_close, NULL);
add_menu_entry(m, "Back", menu_action_close, NULL);
while(!m->entries[m->cursor].action) {
++m->cursor;
}
return m;
}

View file

@ -14,6 +14,6 @@
#include "menu.h"
#include "difficulty.h"
void create_stgpract_menu(MenuData *m, Difficulty diff);
MenuData* create_stgpract_menu(Difficulty diff);
#endif // IGUARD_menu_stagepractice_h

View file

@ -21,11 +21,12 @@ static void draw_stage_menu(MenuData *m) {
draw_menu_list(m, 100, 100, NULL);
}
void create_stage_menu(MenuData *m) {
MenuData* create_stage_menu(void) {
char title[STGMENU_MAX_TITLE_LENGTH];
Difficulty lastdiff = D_Any;
create_menu(m);
MenuData *m = alloc_menu();
m->draw = draw_stage_menu;
m->logic = animate_menu_list;
m->flags = MF_Abortable;
@ -43,5 +44,7 @@ void create_stage_menu(MenuData *m) {
}
add_menu_separator(m);
add_menu_entry(m, "Back", menu_commonaction_close, NULL);
add_menu_entry(m, "Back", menu_action_close, NULL);
return m;
}

View file

@ -15,6 +15,6 @@
#define STGMENU_MAX_TITLE_LENGTH 128
void create_stage_menu(MenuData *m);
MenuData* create_stage_menu(void);
#endif // IGUARD_menu_stageselect_h

View file

@ -18,42 +18,41 @@
#include "global.h"
#include "submenus.h"
void enter_options(MenuData *menu, void *arg) {
MenuData m;
create_options_menu(&m);
menu_loop(&m);
static void on_leave_options(CallChainResult ccr) {
MenuData *m = ccr.result;
if(m->state == MS_Dead) {
taisei_commit_persistent_data();
}
}
void enter_stagemenu(MenuData *menu, void *arg) {
MenuData m;
create_stage_menu(&m);
menu_loop(&m);
void menu_action_enter_options(MenuData *menu, void *arg) {
enter_menu(create_options_menu(), CALLCHAIN(on_leave_options, NULL));
}
void enter_replayview(MenuData *menu, void *arg) {
MenuData m;
create_replayview_menu(&m);
menu_loop(&m);
void menu_action_enter_stagemenu(MenuData *menu, void *arg) {
enter_menu(create_stage_menu(), NO_CALLCHAIN);
}
void enter_spellpractice(MenuData *menu, void *arg) {
MenuData m;
create_spell_menu(&m);
menu_loop(&m);
void menu_action_enter_replayview(MenuData *menu, void *arg) {
enter_menu(create_replayview_menu(), NO_CALLCHAIN);
}
void enter_stagepractice(MenuData *menu, void *arg) {
MenuData m;
do {
create_difficulty_menu(&m);
if(menu_loop(&m) < 0) {
return;
}
global.diff = progress.game_settings.difficulty;
create_stgpract_menu(&m, global.diff);
menu_loop(&m);
} while(m.selected < 0 || m.selected == m.ecount - 1);
void menu_action_enter_spellpractice(MenuData *menu, void *arg) {
enter_menu(create_spell_menu(), NO_CALLCHAIN);
}
static void stgpract_do_choose_stage(CallChainResult ccr);
void menu_action_enter_stagepractice(MenuData *menu, void *arg) {
enter_menu(create_difficulty_menu(), CALLCHAIN(stgpract_do_choose_stage, NULL));
}
static void stgpract_do_choose_stage(CallChainResult ccr) {
MenuData *prev_menu = ccr.result;
assert(prev_menu != NULL);
if(prev_menu->selected >= 0) {
enter_menu(create_stgpract_menu(progress.game_settings.difficulty), NO_CALLCHAIN);
}
}

View file

@ -11,10 +11,10 @@
#include "taisei.h"
void enter_options(MenuData *menu, void *arg);
void enter_stagemenu(MenuData *menu, void *arg);
void enter_replayview(MenuData *menu, void *arg);
void enter_spellpractice(MenuData *menu, void *arg);
void enter_stagepractice(MenuData *menu, void *arg);
void menu_action_enter_options(MenuData *menu, void *arg);
void menu_action_enter_stagemenu(MenuData *menu, void *arg);
void menu_action_enter_replayview(MenuData *menu, void *arg);
void menu_action_enter_spellpractice(MenuData *menu, void *arg);
void menu_action_enter_stagepractice(MenuData *menu, void *arg);
#endif // IGUARD_menu_submenus_h

View file

@ -102,6 +102,7 @@ sse42_src = []
subdir('audio')
subdir('dialog')
subdir('eventloop')
subdir('menu')
subdir('plrmodes')
subdir('renderer')
@ -132,6 +133,7 @@ configure_file(configuration : config, output : 'build_config.h')
taisei_src += [
audio_src,
dialog_src,
eventloop_src,
menu_src,
plrmodes_src,
renderer_src,
@ -148,17 +150,89 @@ taisei_deps += [
util_deps,
]
if macos_app_bundle
taisei_exe_name = 'Taisei'
else
taisei_exe_name = 'taisei'
endif
taisei_basename = (macos_app_bundle ? 'Taisei' : 'taisei')
taisei_exe = executable(taisei_exe_name, taisei_src, version_deps,
dependencies : taisei_deps,
c_args : taisei_c_args,
c_pch : 'pch/taisei_pch.h',
gui_app : not get_option('win_console'),
install : true,
install_dir : bindir,
)
if host_machine.system() == 'emscripten'
em_debug = get_option('debug')
em_link_outputs = []
em_link_output_suffixes = ['html', 'wasm', 'js'] # first element is significant
em_data_dir = config.get_unquoted('TAISEI_BUILDCONF_DATA_PATH')
em_link_args = [
em_bundle_link_args,
'--pre-js', em_preamble,
'--shell-file', em_shell,
'-s', 'ALLOW_MEMORY_GROWTH=1',
'-s', 'ENVIRONMENT=web',
'-s', 'ERROR_ON_MISSING_LIBRARIES=0',
'-s', 'EXIT_RUNTIME=0',
'-s', 'EXPORTED_RUNTIME_METHODS=["ccall"]',
'-s', 'EXPORT_NAME=Taisei',
'-s', 'FILESYSTEM=1',
'-s', 'FORCE_FILESYSTEM=1',
'-s', 'GL_POOL_TEMP_BUFFERS=0',
'-s', 'LZ4=1',
'-s', 'MODULARIZE=0',
'-s', 'TOTAL_MEMORY=268435456',
'-s', 'USE_WEBGL2=1',
'-s', 'WASM=1',
# Try enabling this if unpatched Freetype crashes
# '-s', 'EMULATE_FUNCTION_POINTER_CASTS=1',
]
if em_debug
em_link_output_suffixes += ['wast']
em_link_args += [
'--emrun',
'--profiling',
'-O0',
'-g3',
'-s', 'ASSERTIONS=2',
'-s', 'GL_DEBUG=1',
]
else
em_link_args += [
'--llvm-lto', (get_option('b_lto') ? '3' : '0'),
'-O@0@'.format(get_option('optimization')),
'-g0',
'-s', 'ASSERTIONS=0',
]
endif
foreach suffix : em_link_output_suffixes
em_link_outputs += ['@0@.@1@'.format(taisei_basename, suffix)]
endforeach
taisei = executable('@0@.bc'.format(taisei_basename), taisei_src, version_deps,
dependencies : taisei_deps,
c_args : taisei_c_args,
install : false,
)
taisei_html = custom_target(em_link_outputs[0],
# NOTE: Unfortunately we can't just put 'taisei' directly into the command array.
# Meson then makes an invalid assumption that we are going to execute it ("use as a generator"),
# and aborts because there's no exe wrapper in the cross file (which wouldn't make sense to have).
command : [
cc.cmd_array(),
taisei.full_path(),
em_link_args,
'-o', '@OUTPUT0@'
],
build_by_default : true,
depends : [taisei],
output : em_link_outputs,
install : true,
install_dir : bindir,
)
else
taisei = executable(taisei_basename, taisei_src, version_deps,
dependencies : taisei_deps,
c_args : taisei_c_args,
c_pch : 'pch/taisei_pch.h',
gui_app : not get_option('win_console'),
install : true,
install_dir : bindir,
)
endif

View file

@ -580,7 +580,6 @@ void r_vertex_array_layout(VertexArray *varr, uint nattribs, VertexAttribFormat
}
void r_vsync(VsyncMode mode) {
_r_state_touch_vsync();
B.vsync(mode);
}

View file

@ -26,6 +26,8 @@ typedef struct GLSLParseState {
ShaderSource *src;
SDL_RWops *dest;
bool version_defined;
char *linebuf;
size_t linebuf_size;
} GLSLParseState;
typedef struct GLSLFileParseState {
@ -158,14 +160,13 @@ static bool glsl_process_file(GLSLFileParseState *fstate) {
return false;
}
// TODO: Remove this dumb limitation; also handle comments and line continuations properly.
char linebuf[1024];
// TODO: Handle comments and line continuations properly.
++fstate->lineno;
glsl_write_lineno(fstate);
while(SDL_RWgets(stream, linebuf, sizeof(linebuf))) {
char *p = linebuf;
while(SDL_RWgets_realloc(stream, &fstate->global->linebuf, &fstate->global->linebuf_size)) {
char *p = fstate->global->linebuf;
skip_space(&p);
if(check_directive(p, INCLUDE_DIRECTIVE)) {
@ -232,7 +233,7 @@ static bool glsl_process_file(GLSLFileParseState *fstate) {
);
fstate->global->src->lang.glsl.version = opt_v;
} else {
log_warn(
log_debug(
"%s:%d: source overrides version to %s (default is %s)",
fstate->path, fstate->lineno, buf_shader, buf_opt
);
@ -250,7 +251,7 @@ static bool glsl_process_file(GLSLFileParseState *fstate) {
return false;
}
SDL_RWwrite(dest, linebuf, 1, strlen(linebuf));
SDL_RWwrite(dest, fstate->global->linebuf, 1, strlen(fstate->global->linebuf));
}
fstate->lineno++;
@ -276,12 +277,15 @@ bool glsl_load_source(const char *path, ShaderSource *out, const GLSLSourceOptio
pstate.dest = out_buf;
pstate.src = out;
pstate.options = options;
pstate.linebuf_size = 128;
pstate.linebuf = calloc(1, pstate.linebuf_size);
GLSLFileParseState fstate = { 0 };
fstate.global = &pstate;
fstate.path = path;
bool result = glsl_process_file(&fstate);
free(pstate.linebuf);
if(result) {
SDL_WriteU8(out_buf, 0);

View file

@ -89,10 +89,6 @@ void r_state_pop(void) {
B.framebuffer(S.framebuffer);
}
RESTORE(RSTATE_VSYNC) {
B.vsync(S.vsync);
}
if(_r_state.head == _r_state.stack) {
_r_state.head = NULL;
} else {
@ -150,9 +146,3 @@ void _r_state_touch_framebuffer(void) {
S.framebuffer = B.framebuffer_current();
});
}
void _r_state_touch_vsync(void) {
TAINT(RSTATE_VSYNC, {
S.vsync = B.vsync_current();
});
}

View file

@ -23,7 +23,6 @@
RSTATE(SHADER) \
RSTATE(SHADER_UNIFORMS) \
RSTATE(RENDERTARGET) \
RSTATE(VSYNC) \
typedef enum RendererStateID {
#define RSTATE(id) RSTATE_ID_##id,
@ -52,7 +51,6 @@ typedef struct RendererStateRollback {
ShaderProgram *shader;
// TODO uniforms
Framebuffer *framebuffer;
VsyncMode vsync;
} RendererStateRollback;
void _r_state_touch_capabilities(void);
@ -64,7 +62,6 @@ void _r_state_touch_depth_func(void);
void _r_state_touch_shader(void);
void _r_state_touch_uniform(Uniform *uniform);
void _r_state_touch_framebuffer(void);
void _r_state_touch_vsync(void);
void _r_state_init(void);
void _r_state_shutdown(void);

View file

@ -971,6 +971,14 @@ static void gl33_swap(SDL_Window *window) {
gl33_sync_framebuffer();
SDL_GL_SwapWindow(window);
gl33_stats_post_frame();
if(glext.version.is_webgl) {
// We can't rely on viewport being preserved across frames,
// so force the next frame to set one on the first draw call.
// The viewport might get updated externally when e.g. going
// fullscreen, and we can't catch that in the resize event.
memset(&R.viewport.active, 0, sizeof(R.viewport.active));
}
}
static void gl33_blend(BlendMode mode) {

View file

@ -333,7 +333,20 @@ static bool cache_uniforms(ShaderProgram *prog) {
uni.cache.pending = calloc(uni.array_size, uni.elem_size);
uni.cache.update_first_idx = uni.array_size;
type_to_accessors[uni.type].getter(&uni, size, uni.cache.commited);
if(glext.version.is_webgl) {
// Some browsers are pendatic about getting a null in GLctx.getUniform(),
// so we'd have to be very careful and query each array index with
// glGetUniformLocation in order to avoid an exception. Which is too much
// hassle, so instead here's a hack that fills initial cache state with
// some garbage that we'll not likely want to actually set.
//
// TODO: Might want to fix this properly if this issue ever actually
// affects cases where we write to an array with an offset. But that's
// probably not going to happen.
memset(uni.cache.commited, 0xf0, uni.array_size * uni.elem_size);
} else {
type_to_accessors[uni.type].getter(&uni, size, uni.cache.commited);
}
Uniform *new_uni = memdup(&uni, sizeof(uni));

View file

@ -503,6 +503,7 @@ void glcommon_check_extensions(void) {
if(glext.version.is_es) {
glext.version.is_ANGLE = strstr(glv, "(ANGLE ");
glext.version.is_webgl = strstr(glv, "(WebGL ");
}
log_info("OpenGL version: %s", glv);

View file

@ -71,6 +71,7 @@ struct glext_s {
char minor;
bool is_es;
bool is_ANGLE;
bool is_webgl;
} version;
ext_flag_t base_instance;

View file

@ -642,6 +642,7 @@ bool replay_save(Replay *rpy, const char *name) {
bool result = replay_write(rpy, file, REPLAY_STRUCT_VERSION_WRITE);
SDL_RWclose(file);
vfs_sync(VFS_SYNC_STORE, NO_CALLCHAIN);
return result;
}
@ -783,45 +784,87 @@ int replay_find_stage_idx(Replay *rpy, uint8_t stageid) {
return -1;
}
void replay_play(Replay *rpy, int firstidx) {
typedef struct ReplayContext {
CallChain cc;
int stage_idx;
} ReplayContext;
static void replay_do_cleanup(CallChainResult ccr);
static void replay_do_play(CallChainResult ccr);
static void replay_do_post_play(CallChainResult ccr);
void replay_play(Replay *rpy, int firstidx, CallChain next) {
if(rpy != &global.replay) {
replay_copy(&global.replay, rpy, true);
}
if(firstidx >= global.replay.numstages || firstidx < 0) {
log_error("No stage #%i in the replay", firstidx);
replay_destroy(&global.replay);
run_call_chain(&next, NULL);
return;
}
global.replaymode = REPLAY_PLAY;
for(int i = firstidx; i < global.replay.numstages; ++i) {
ReplayStage *rstg = global.replay_stage = global.replay.stages+i;
StageInfo *gstg = stage_get(rstg->stage);
ReplayContext *ctx = calloc(1, sizeof(*ctx));
ctx->cc = next;
ctx->stage_idx = firstidx;
replay_do_play(CALLCHAIN_RESULT(ctx, NULL));
}
static void replay_do_play(CallChainResult ccr) {
ReplayContext *ctx = ccr.ctx;
ReplayStage *rstg = NULL;
StageInfo *gstg = NULL;
while(ctx->stage_idx < global.replay.numstages) {
rstg = global.replay_stage = global.replay.stages + ctx->stage_idx++;
gstg = stage_get(rstg->stage);
if(!gstg) {
log_warn("Invalid stage %X in replay at %i skipped.", rstg->stage, i);
log_warn("Invalid stage %X in replay at %i skipped.", rstg->stage, ctx->stage_idx);
continue;
}
global.plr.mode = plrmode_find(rstg->plr_char, rstg->plr_shot);
stage_loop(gstg);
if(global.gameover == GAMEOVER_ABORT) {
break;
}
if(global.gameover == GAMEOVER_RESTART) {
rstg->desynced = false;
--i;
}
global.gameover = 0;
break;
}
if(gstg == NULL) {
replay_do_cleanup(ccr);
} else {
global.plr.mode = plrmode_find(rstg->plr_char, rstg->plr_shot);
stage_enter(gstg, CALLCHAIN(replay_do_post_play, ctx));
}
}
static void replay_do_post_play(CallChainResult ccr) {
ReplayContext *ctx = ccr.ctx;
if(global.gameover == GAMEOVER_ABORT) {
replay_do_cleanup(ccr);
return;
}
if(global.gameover == GAMEOVER_RESTART) {
--ctx->stage_idx;
}
global.gameover = 0;
replay_do_play(ccr);
}
static void replay_do_cleanup(CallChainResult ccr) {
ReplayContext *ctx = ccr.ctx;
global.gameover = 0;
global.replaymode = REPLAY_RECORD;
replay_destroy(&global.replay);
global.replay_stage = NULL;
free_resources(false);
CallChain cc = ctx->cc;
free(ctx);
run_call_chain(&cc, NULL);
}

View file

@ -242,7 +242,7 @@ bool replay_load_syspath(Replay *rpy, const char *path, ReplayReadMode mode);
void replay_copy(Replay *dst, Replay *src, bool steal_events);
void replay_play(Replay *rpy, int firstidx);
void replay_play(Replay *rpy, int firstidx, CallChain next);
int replay_find_stage_idx(Replay *rpy, uint8_t stageid);

View file

@ -25,6 +25,7 @@
#include "stagetext.h"
#include "stagedraw.h"
#include "stageobjects.h"
#include "eventloop/eventloop.h"
#ifdef DEBUG
#define DPSTEST
@ -240,13 +241,12 @@ static void stage_fade_bgm(void) {
fade_bgm((FPS * FADE_TIME) / 2000.0);
}
static void stage_ingame_menu_loop(MenuData *menu) {
if(ingame_menu_interrupts_bgm()) {
stop_bgm(false);
}
static void stage_leave_ingame_menu(CallChainResult ccr) {
MenuData *m = ccr.result;
pause_sounds();
menu_loop(menu);
if(m->state != MS_Dead) {
return;
}
if(global.gameover > 0) {
stop_sounds();
@ -258,6 +258,19 @@ static void stage_ingame_menu_loop(MenuData *menu) {
resume_sounds();
resume_bgm();
}
CallChain *cc = ccr.ctx;
run_call_chain(cc, NULL);
free(cc);
}
static void stage_enter_ingame_menu(MenuData *m, CallChain next) {
if(ingame_menu_interrupts_bgm()) {
stop_bgm(false);
}
pause_sounds();
enter_menu(m, CALLCHAIN(stage_leave_ingame_menu, memdup(&next, sizeof(next))));
}
void stage_pause(void) {
@ -265,15 +278,12 @@ void stage_pause(void) {
return;
}
MenuData menu;
if(global.replaymode == REPLAY_PLAY) {
create_ingame_menu_replay(&menu);
} else {
create_ingame_menu(&menu);
}
stage_ingame_menu_loop(&menu);
stage_enter_ingame_menu(
(global.replaymode == REPLAY_PLAY
? create_ingame_menu_replay
: create_ingame_menu
)(), NO_CALLCHAIN
);
}
void stage_gameover(void) {
@ -282,9 +292,7 @@ void stage_gameover(void) {
return;
}
MenuData menu;
create_gameover_menu(&menu);
stage_ingame_menu_loop(&menu);
stage_enter_ingame_menu(create_gameover_menu(), NO_CALLCHAIN);
}
static bool stage_input_common(SDL_Event *event, void *arg) {
@ -635,6 +643,8 @@ typedef struct StageFrameState {
StageInfo *stage;
int transition_delay;
uint16_t last_replay_fps;
CallChain cc;
int logic_calls;
} StageFrameState;
static void stage_update_fps(StageFrameState *fstate) {
@ -678,10 +688,12 @@ static void stage_give_clear_bonus(const StageInfo *stage, StageClearBonus *bonu
player_add_points(&global.plr, bonus->total);
}
static FrameAction stage_logic_frame(void *arg) {
static LogicFrameAction stage_logic_frame(void *arg) {
StageFrameState *fstate = arg;
StageInfo *stage = fstate->stage;
++fstate->logic_calls;
stage_update_fps(fstate);
if(global.shake_view > 30) {
@ -742,7 +754,7 @@ static FrameAction stage_logic_frame(void *arg) {
return LFRAME_WAIT;
}
static FrameAction stage_render_frame(void *arg) {
static RenderFrameAction stage_render_frame(void *arg) {
StageFrameState *fstate = arg;
StageInfo *stage = fstate->stage;
@ -758,7 +770,9 @@ static FrameAction stage_render_frame(void *arg) {
return RFRAME_SWAP;
}
void stage_loop(StageInfo *stage) {
static void stage_end_loop(void *ctx);
void stage_enter(StageInfo *stage, CallChain next) {
assert(stage);
assert(stage->procs);
assert(stage->procs->preload);
@ -772,6 +786,7 @@ void stage_loop(StageInfo *stage) {
if(global.gameover == GAMEOVER_WIN) {
global.gameover = 0;
} else if(global.gameover) {
run_call_chain(&next, NULL);
return;
}
@ -834,8 +849,15 @@ void stage_loop(StageInfo *stage) {
display_stage_title(stage);
}
StageFrameState fstate = { .stage = stage };
loop_at_fps(stage_logic_frame, stage_render_frame, &fstate, FPS);
StageFrameState *fstate = calloc(1 , sizeof(*fstate));
fstate->stage = stage;
fstate->cc = next;
eventloop_enter(fstate, stage_logic_frame, stage_render_frame, stage_end_loop, FPS);
}
void stage_end_loop(void* ctx) {
StageFrameState *s = ctx;
if(global.replaymode == REPLAY_RECORD) {
replay_stage_event(global.replay_stage, global.frames, EV_OVER, 0);
@ -845,7 +867,7 @@ void stage_loop(StageInfo *stage) {
}
}
stage->procs->end();
s->stage->procs->end();
stage_draw_shutdown();
stage_free();
player_free(&global.plr);
@ -854,8 +876,12 @@ void stage_loop(StageInfo *stage) {
ent_shutdown();
stage_objpools_free();
stop_sounds();
taisei_commit_persistent_data();
if(taisei_quit_requested()) {
global.gameover = GAMEOVER_ABORT;
}
run_call_chain(&s->cc, NULL);
free(s);
}

View file

@ -119,7 +119,7 @@ StageProgress* stage_get_progress_from_info(StageInfo *stage, Difficulty diff, b
void stage_init_array(void);
void stage_free_array(void);
void stage_loop(StageInfo *stage);
void stage_enter(StageInfo *stage, CallChain next);
void stage_finish(int gameover);
void stage_pause(void);

View file

@ -170,6 +170,9 @@ typedef complex max_align_t;
#else
#define CMPLX(re,im) (_Complex double)((double)(re) + _Complex_I * (double)(im))
#endif
#elif defined __EMSCRIPTEN__ && defined __clang__
// CMPLX from emscripten headers uses the clang-specific syntax without __extension__
#pragma clang diagnostic ignored "-Wcomplex-component-init"
#endif
/*
@ -197,10 +200,14 @@ typedef complex max_align_t;
#define attr_sentinel \
__attribute__ ((sentinel))
// Identifier is meant to be possibly unused.
// Symbol is meant to be possibly unused.
#define attr_unused \
__attribute__ ((unused))
// Symbol should be emitted even if it appears to be unused.
#define attr_used \
__attribute__ ((used))
// Function or type is deprecated and should not be used.
#define attr_deprecated(msg) \
__attribute__ ((deprecated(msg)))

View file

@ -18,7 +18,9 @@
uint line;
} DebugInfo;
#define _DEBUG_INFO_PTR_ (&(DebugInfo){ __FILE__, __func__, __LINE__ })
#define _DEBUG_INFO_INITIALIZER_ { __FILE__, __func__, __LINE__ }
#define _DEBUG_INFO_ ((DebugInfo) _DEBUG_INFO_INITIALIZER_)
#define _DEBUG_INFO_PTR_ (&_DEBUG_INFO_)
#define set_debug_info(debug) _set_debug_info(debug, _DEBUG_INFO_PTR_)
void _set_debug_info(DebugInfo *debug, DebugInfo *meta);
DebugInfo* get_debug_info(void);

View file

@ -62,6 +62,36 @@ char* SDL_RWgets(SDL_RWops *rwops, char *buf, size_t bufsize) {
return buf;
}
char* SDL_RWgets_realloc(SDL_RWops *rwops, char **buf, size_t *bufsize) {
char c, *ptr = *buf, *end = *buf + *bufsize - 1;
assert(end >= ptr);
while((c = SDL_ReadU8(rwops))) {
*ptr++ = c;
if(ptr > end) {
ptrdiff_t ofs = ptr - *buf;
*bufsize *= 2;
*buf = realloc(*buf, *bufsize);
end = *buf + *bufsize - 1;
ptr = *buf + ofs;
*end = 0;
}
if(c == '\n') {
break;
}
}
if(ptr == *buf)
return NULL;
assert(ptr <= end);
*ptr = 0;
return *buf;
}
size_t SDL_RWprintf(SDL_RWops *rwops, const char* fmt, ...) {
va_list args;
va_start(args, fmt);

View file

@ -16,6 +16,7 @@
char* read_all(const char *filename, int *size);
char* SDL_RWgets(SDL_RWops *rwops, char *buf, size_t bufsize);
char* SDL_RWgets_realloc(SDL_RWops *rwops, char **buf, size_t *bufsize);
size_t SDL_RWprintf(SDL_RWops *rwops, const char* fmt, ...) attr_printf(2, 3);
// This is for the very few legitimate uses for printf/fprintf that shouldn't be replaced with log_*

View file

@ -5,7 +5,6 @@ vfs_src = files(
'private.c',
'public.c',
'readonly_wrapper.c',
'setup.c',
'syspath_public.c',
'union.c',
'union_public.c',
@ -31,3 +30,11 @@ elif host_machine.system() == 'windows'
else
vfs_src += files('syspath_posix.c') # eeehh, maybe it'll work ¯\_(ツ)_/¯
endif
if host_machine.system() == 'emscripten'
vfs_src += files('setup_emscripten.c')
vfs_src += files('sync_emscripten.c')
else
vfs_src += files('setup_generic.c')
vfs_src += files('sync_noop.c')
endif

View file

@ -76,6 +76,8 @@ static void* call_shutdown_hook(List **vlist, List *vhook, void *arg) {
}
void vfs_shutdown(void) {
vfs_sync(VFS_SYNC_STORE, NO_CALLCHAIN);
list_foreach(&shutdown_hooks, call_shutdown_hook, NULL);
vfs_decref(vfs_root);

View file

@ -197,7 +197,7 @@ const char* vfs_dir_read(VFSDir *dir) {
return vfs_node_iter(dir->node, &dir->opaque);
}
char** vfs_dir_list_sorted(const char *path, size_t *out_size, int (*compare)(const char**, const char**), bool (*filter)(const char*)) {
char** vfs_dir_list_sorted(const char *path, size_t *out_size, int (*compare)(const void*, const void*), bool (*filter)(const char*)) {
char **results = NULL;
VFSDir *dir = vfs_dir_open(path);
@ -225,7 +225,7 @@ char** vfs_dir_list_sorted(const char *path, size_t *out_size, int (*compare)(co
vfs_dir_close(dir);
if(*out_size) {
qsort(results, *out_size, sizeof(char*), (int (*)(const void*, const void*)) compare);
qsort(results, *out_size, sizeof(char*), compare);
}
return results;
@ -243,12 +243,12 @@ void vfs_dir_list_free(char **list, size_t size) {
free(list);
}
int vfs_dir_list_order_ascending(const char **a, const char **b) {
return strcmp(*a, *b);
int vfs_dir_list_order_ascending(const void *a, const void *b) {
return strcmp(*(char**)a, *(char**)b);
}
int vfs_dir_list_order_descending(const char **a, const char **b) {
return strcmp(*b, *a);
int vfs_dir_list_order_descending(const void *a, const void *b) {
return strcmp(*(char**)b, *(char**)a);
}
void* vfs_dir_walk(const char *path, void* (*visit)(const char *path, void *arg), void *arg) {

View file

@ -17,6 +17,7 @@
#include "union_public.h"
#include "zipfile_public.h"
#include "readonly_wrapper_public.h"
#include "eventloop/eventloop.h"
typedef struct VFSInfo {
uchar error : 1;
@ -33,6 +34,11 @@ typedef enum VFSOpenMode {
VFS_MODE_SEEKABLE = 4,
} VFSOpenMode;
typedef enum VFSSyncMode {
VFS_SYNC_LOAD = 1,
VFS_SYNC_STORE = 0,
} VFSSyncMode;
#define VFS_MODE_RWMASK (VFS_MODE_READ | VFS_MODE_WRITE)
typedef struct VFSDir VFSDir;
@ -52,11 +58,11 @@ const char* vfs_dir_read(VFSDir *dir) attr_nonnull(1);
void* vfs_dir_walk(const char *path, void* (*visit)(const char *path, void *arg), void *arg);
char** vfs_dir_list_sorted(const char *path, size_t *out_size, int (*compare)(const char**, const char**), bool (*filter)(const char*))
char** vfs_dir_list_sorted(const char *path, size_t *out_size, int (*compare)(const void*, const void*), bool (*filter)(const char*))
attr_nonnull(1, 2, 3) attr_nodiscard;
void vfs_dir_list_free(char **list, size_t size);
int vfs_dir_list_order_ascending(const char **a, const char **b);
int vfs_dir_list_order_descending(const char **a, const char **b);
int vfs_dir_list_order_ascending(const void *a, const void *b);
int vfs_dir_list_order_descending(const void *a, const void *b);
char* vfs_repr(const char *path, bool try_syspath) attr_nonnull(1) attr_nodiscard;
bool vfs_print_tree(SDL_RWops *dest, const char *path) attr_nonnull(1, 2);
@ -66,4 +72,6 @@ void vfs_init(void);
void vfs_shutdown(void);
const char* vfs_get_error(void) attr_returns_nonnull;
void vfs_sync(VFSSyncMode mode, CallChain next);
#endif // IGUARD_vfs_public_h

View file

@ -11,8 +11,8 @@
#include "taisei.h"
#include "util.h"
#include "public.h"
void vfs_setup(bool silent);
void vfs_setup(CallChain onready);
#endif // IGUARD_vfs_setup_h

View file

@ -0,0 +1,41 @@
#include "taisei.h"
#include "public.h"
#include "setup.h"
#include "util.h"
static void vfs_setup_onsync(CallChainResult ccr) {
const char *res_path = "/" TAISEI_BUILDCONF_DATA_PATH;
const char *storage_path = "/persistent/storage";
const char *cache_path = "/persistent/cache";
log_info("Resource path: %s", res_path);
log_info("Storage path: %s", storage_path);
log_info("Cache path: %s", cache_path);
if(!vfs_mount_syspath("/res", res_path, VFS_SYSPATH_MOUNT_READONLY)) {
log_fatal("Failed to mount '%s': %s", res_path, vfs_get_error());
}
if(!vfs_mount_syspath("/storage", storage_path, VFS_SYSPATH_MOUNT_MKDIR)) {
log_fatal("Failed to mount '%s': %s", storage_path, vfs_get_error());
}
if(!vfs_mount_syspath("/cache", cache_path, VFS_SYSPATH_MOUNT_MKDIR)) {
log_fatal("Failed to mount '%s': %s", cache_path, vfs_get_error());
}
vfs_mkdir_required("storage/replays");
vfs_mkdir_required("storage/screenshots");
CallChain *next = ccr.ctx;
run_call_chain(next, NULL);
free(next);
}
void vfs_setup(CallChain next) {
vfs_init();
CallChain *cc = memdup(&next, sizeof(next));
vfs_sync(VFS_SYNC_LOAD, CALLCHAIN(vfs_setup_onsync, cc));
}

View file

@ -8,7 +8,6 @@
#include "taisei.h"
#include "build_config.h"
#include "public.h"
#include "setup.h"
#include "error.h"
@ -132,18 +131,18 @@ static void load_packages(const char *dir, const char *unionmp) {
vfs_dir_list_free(paklist, numpaks);
}
void vfs_setup(bool silent) {
// NOTE: For simplicity, we will assume that vfs_sync is not needed in this backend.
void vfs_setup(CallChain next) {
char *res_path, *storage_path, *cache_path;
get_core_paths(&res_path, &storage_path, &cache_path);
char *local_res_path = strfmt("%s/resources", storage_path);
if(!silent) {
log_info("Resource path: %s", res_path);
log_info("Storage path: %s", storage_path);
log_info("Local resource path: %s", local_res_path);
log_info("Cache path: %s", cache_path);
}
log_info("Resource path: %s", res_path);
log_info("Storage path: %s", storage_path);
log_info("Local resource path: %s", local_res_path);
log_info("Cache path: %s", cache_path);
struct mpoint_t {
const char *dest; const char *syspath; bool loadpaks; uint flags;
@ -215,4 +214,6 @@ void vfs_setup(bool silent) {
vfs_unmount("resdirs");
vfs_unmount("respkgs");
run_call_chain(&next, NULL);
}

32
src/vfs/sync_emscripten.c Normal file
View file

@ -0,0 +1,32 @@
#include "public.h"
#include "util.h"
#include <emscripten.h>
void EMSCRIPTEN_KEEPALIVE vfs_sync_callback(bool is_load, char *error, CallChain *next);
void vfs_sync_callback(bool is_load, char *error, CallChain *next) {
if(error) {
if(is_load) {
log_error("Couldn't load persistent storage from IndexedDB: %s", error);
} else {
log_error("Couldn't save persistent storage to IndexedDB: %s", error);
}
} else {
if(is_load) {
log_info("Loaded persistent storage from IndexedDB");
} else {
log_info("Saved persistent storage to IndexedDB");
}
}
run_call_chain(next, error);
free(next);
}
void vfs_sync(VFSSyncMode mode, CallChain next) {
CallChain *cc = memdup(&next, sizeof(next));
__extension__ (EM_ASM({
SyncFS($0, $1);
}, (mode == VFS_SYNC_LOAD), cc));
}

6
src/vfs/sync_noop.c Normal file
View file

@ -0,0 +1,6 @@
#include "public.h"
void vfs_sync(VFSSyncMode mode, CallChain next) {
run_call_chain(&next, NULL);
}

View file

@ -23,6 +23,62 @@ typedef struct ScreenshotTaskData {
Pixmap image;
} ScreenshotTaskData;
VideoCapabilityState (*video_query_capability)(VideoCapability cap);
static VideoCapabilityState video_query_capability_generic(VideoCapability cap) {
switch(cap) {
case VIDEO_CAP_FULLSCREEN:
return VIDEO_AVAILABLE;
case VIDEO_CAP_EXTERNAL_RESIZE:
return video_is_fullscreen() ? VIDEO_CURRENTLY_UNAVAILABLE : VIDEO_AVAILABLE;
case VIDEO_CAP_CHANGE_RESOLUTION:
if(video_is_fullscreen() && config_get_int(CONFIG_FULLSCREEN_DESKTOP)) {
return VIDEO_CURRENTLY_UNAVAILABLE;
} else {
return VIDEO_AVAILABLE;
}
case VIDEO_CAP_VSYNC_ADAPTIVE:
return VIDEO_AVAILABLE;
}
UNREACHABLE;
}
static VideoCapabilityState video_query_capability_alwaysfullscreen(VideoCapability cap) {
switch(cap) {
case VIDEO_CAP_FULLSCREEN:
return VIDEO_ALWAYS_ENABLED;
case VIDEO_CAP_EXTERNAL_RESIZE:
return VIDEO_NEVER_AVAILABLE;
// XXX: Might not be actually working, but let's be optimistic.
case VIDEO_CAP_CHANGE_RESOLUTION:
return VIDEO_AVAILABLE;
case VIDEO_CAP_VSYNC_ADAPTIVE:
return VIDEO_AVAILABLE;
}
UNREACHABLE;
}
static VideoCapabilityState video_query_capability_webcanvas(VideoCapability cap) {
switch(cap) {
case VIDEO_CAP_EXTERNAL_RESIZE:
return VIDEO_NEVER_AVAILABLE;
case VIDEO_CAP_VSYNC_ADAPTIVE:
return VIDEO_NEVER_AVAILABLE;
default:
return video_query_capability_generic(cap);
}
}
static void video_add_mode(int width, int height) {
if(video.modes) {
for(uint i = 0; i < video.mcount; ++i) {
@ -158,7 +214,7 @@ static void video_new_window(int w, int h, bool fs, bool resizable) {
if(fs) {
flags |= get_fullscreen_flag();
} else if(resizable) {
} else if(resizable && video.backend != VIDEO_BACKEND_EMSCRIPTEN) {
flags |= SDL_WINDOW_RESIZABLE;
}
@ -195,7 +251,7 @@ static bool video_set_display_mode(int w, int h) {
return true;
}
static void video_set_fullscreen(bool fullscreen) {
static void video_set_fullscreen_internal(bool fullscreen) {
uint32_t flags = fullscreen ? get_fullscreen_flag() : 0;
events_pause_keyrepeat();
@ -218,9 +274,9 @@ void video_set_mode(int w, int h, bool fs, bool resizable) {
if(w != video.current.width || h != video.current.height) {
if(fs && !config_get_int(CONFIG_FULLSCREEN_DESKTOP)) {
video_set_display_mode(w, h);
video_set_fullscreen(fs);
video_set_fullscreen_internal(fs);
video_update_mode_settings();
} else {
} else if(video.backend == VIDEO_BACKEND_X11) {
// XXX: I would like to use SDL_SetWindowSize for size changes, but apparently it's impossible to reliably detect
// when it fails to actually resize the window. For example, a tiling WM (awesome) may be getting in its way
// and we'd never know. SDL_GL_GetDrawableSize/SDL_GetWindowSize aren't helping as of SDL 2.0.5.
@ -228,13 +284,24 @@ void video_set_mode(int w, int h, bool fs, bool resizable) {
// There's not much to be done about it. We're at mercy of SDL here and SDL is at mercy of the WM.
video_new_window(w, h, fs, resizable);
return;
} else if(video.backend == VIDEO_BACKEND_EMSCRIPTEN && !fs) {
// Needed to work around various SDL bugs and HTML/DOM quirks...
video_new_window(w, h, fs, resizable);
return;
} else {
SDL_SetWindowSize(video.window, w, h);
video_update_mode_settings();
}
}
video_set_fullscreen(fs);
video_set_fullscreen_internal(fs);
SDL_SetWindowResizable(video.window, resizable);
}
void video_set_fullscreen(bool fullscreen) {
video_set_mode(video.intended.width, video.intended.height, fullscreen, video_is_resizable());
}
static void* video_screenshot_task(void *arg) {
ScreenshotTaskData *tdata = arg;
@ -350,10 +417,6 @@ bool video_is_fullscreen(void) {
return WINFLAGS_IS_FULLSCREEN(SDL_GetWindowFlags(video.window));
}
bool video_can_change_resolution(void) {
return !video_is_fullscreen() || !config_get_int(CONFIG_FULLSCREEN_DESKTOP);
}
static void video_init_sdl(void) {
// XXX: workaround for an SDL bug: https://bugzilla.libsdl.org/show_bug.cgi?id=4127
SDL_SetHintWithPriority(SDL_HINT_FRAMEBUFFER_ACCELERATION, "0", SDL_HINT_OVERRIDE);
@ -421,6 +484,8 @@ static void video_handle_resize(int w, int h) {
if(w < minw || h < minh) {
log_warn("Bad resize: %ix%i is too small!", w, h);
// FIXME: the video_new_window is actually a workaround for Wayland.
// I'm not sure if it's necessary for anything else.
video_new_window(video.intended.width, video.intended.height, false, video_is_resizable());
return;
}
@ -453,12 +518,7 @@ static bool video_handle_config_event(SDL_Event *evt, void *arg) {
switch(evt->user.code) {
case CONFIG_FULLSCREEN:
video_set_mode(
config_get_int(CONFIG_VID_WIDTH),
config_get_int(CONFIG_VID_HEIGHT),
val->i,
config_get_int(CONFIG_VID_RESIZABLE)
);
video_set_fullscreen(val->i);
break;
case CONFIG_VID_RESIZABLE:
@ -477,7 +537,30 @@ void video_init(void) {
bool fullscreen_available = false;
video_init_sdl();
log_info("Using driver '%s'", SDL_GetCurrentVideoDriver());
const char *driver = SDL_GetCurrentVideoDriver();
log_info("Using driver '%s'", driver);
video_query_capability = video_query_capability_generic;
if(!strcmp(driver, "x11")) {
video.backend = VIDEO_BACKEND_X11;
} else if(!strcmp(driver, "emscripten")) {
video.backend = VIDEO_BACKEND_EMSCRIPTEN;
video_query_capability = video_query_capability_webcanvas;
// We can not start in fullscreen in the browser properly, so disable it here.
// Fullscreen is still accessible via the settings menu and the shortcut key.
config_set_int(CONFIG_FULLSCREEN, false);
} else if(!strcmp(driver, "KMSDRM")) {
video.backend = VIDEO_BACKEND_KMSDRM;
video_query_capability = video_query_capability_alwaysfullscreen;
} else if(!strcmp(driver, "RPI")) {
video.backend = VIDEO_BACKEND_RPI;
video_query_capability = video_query_capability_alwaysfullscreen;
} else {
video.backend = VIDEO_BACKEND_OTHER;
}
r_init();
@ -559,4 +642,7 @@ void video_shutdown(void) {
void video_swap_buffers(void) {
r_framebuffer(NULL);
r_swap(video.window);
// XXX: Unfortunately, there seems to be no reliable way to sync this up with events
config_set_int(CONFIG_FULLSCREEN, video_is_fullscreen());
}

View file

@ -33,24 +33,48 @@ typedef struct VideoMode {
int height;
} VideoMode;
typedef enum VideoBackend {
VIDEO_BACKEND_OTHER,
VIDEO_BACKEND_X11,
VIDEO_BACKEND_EMSCRIPTEN,
VIDEO_BACKEND_KMSDRM,
VIDEO_BACKEND_RPI,
} VideoBackend;
typedef struct {
VideoMode *modes;
SDL_Window *window;
int mcount;
VideoMode intended;
VideoMode current;
SDL_Window *window;
VideoBackend backend;
} Video;
typedef enum VideoCapability {
VIDEO_CAP_FULLSCREEN,
VIDEO_CAP_EXTERNAL_RESIZE,
VIDEO_CAP_CHANGE_RESOLUTION,
VIDEO_CAP_VSYNC_ADAPTIVE,
} VideoCapability;
typedef enum VideoCapabilityState {
VIDEO_NEVER_AVAILABLE,
VIDEO_AVAILABLE,
VIDEO_ALWAYS_ENABLED,
VIDEO_CURRENTLY_UNAVAILABLE,
} VideoCapabilityState;
extern Video video;
void video_init(void);
void video_shutdown(void);
void video_set_mode(int w, int h, bool fs, bool resizable);
void video_set_fullscreen(bool fullscreen);
void video_get_viewport(IntRect *vp);
void video_get_viewport_size(int *width, int *height);
bool video_is_fullscreen(void);
bool video_is_resizable(void);
bool video_can_change_resolution(void);
extern VideoCapabilityState (*video_query_capability)(VideoCapability cap);
void video_take_screenshot(void);
void video_swap_buffers(void);