Compare commits
40 Commits
Author | SHA1 | Date | |
---|---|---|---|
22c1a82e60 | |||
88bbcab1f3 | |||
f11bd999be | |||
3a66572396 | |||
c95e1a010d | |||
0afd882a86 | |||
67af1dd191 | |||
10621e5811 | |||
6e4a52b2d0 | |||
3b43ff1178 | |||
808228173a | |||
39aa59803b | |||
81cd905da1 | |||
6e96d09eaf | |||
89ba7f5944 | |||
9b58ede58f | |||
4818439acb | |||
c5e0b59ef2 | |||
bae10f332e | |||
6ac98c3e7a | |||
0eccdac932 | |||
830c38196b | |||
8b49db0d10 | |||
89ae07b190 | |||
37b6d60811 | |||
138d53bf9b | |||
22bf4f92d4 | |||
19fe406f21 | |||
27c1d752e2 | |||
b202c03c55 | |||
cc18bc56b5 | |||
7efa02654f | |||
0b2e47b36c | |||
bbb3a74b8f | |||
a622071a6a | |||
2acf244175 | |||
290b342ec6 | |||
49d302d801 | |||
8caf8a5916 | |||
5dd3605586 |
192
Makefile
@ -1,17 +1,17 @@
|
||||
# ,~~~~~. | C A K E F O O T <presented by> 💫dank.game💫
|
||||
# |)~)~)| |
|
||||
# |\~~~~: | Licensed under the zlib and CC-BY licenses. Source is available for modding at
|
||||
# |\\~~~| |
|
||||
# |#\\~~: | <https://open.shampoo.ooo/shampoo/cakefoot>
|
||||
# \\#\\~| |
|
||||
# \\#\\: | Created with open SPACE🪐BOX engine for cross-platform, PC, web and mobile games
|
||||
# \\#\' |
|
||||
# \\#/ | <https://open.shampoo.ooo/shampoo/spacebox>
|
||||
#------`/---+
|
||||
#
|
||||
# /*@~@~@ C A K E F O O T <presented by> 💫dank.game💫
|
||||
# |~)~)~)
|
||||
# |\~*~*| Licensed under the zlib and CC-BY licenses. Source is available for modding at
|
||||
# |\\~*~|
|
||||
# ,~|#\\~*|~, <https://open.shampoo.ooo/shampoo/cakefoot>
|
||||
# : \\@\\~| :
|
||||
# : \\#\\| : Created with open SPACE🪐BOX engine for cross-platform, PC, web and mobile games
|
||||
# : \\@\' :
|
||||
# : \\/ : <https://open.shampoo.ooo/shampoo/spacebox>
|
||||
# `~ ~ ~`~ ~'
|
||||
#
|
||||
# This Makefile is used to create executables for Cakefoot. There are targets for Linux, Windows, macOS, and WebGL.
|
||||
#
|
||||
# NOTE: The Android rules are outdated.
|
||||
# NOTE: The Android rules do not work yet - they were copied in from a previous project.
|
||||
#
|
||||
# The SPACE🪐BOX library <https://open.shampoo.ooo/shampoo/spacebox> is required to be installed at the paths in the
|
||||
# Paths section.
|
||||
@ -25,7 +25,7 @@ NAME = Cakefoot
|
||||
|
||||
# Print a success message
|
||||
define success_message
|
||||
@echo "\n✅ $@ succeeded"
|
||||
@echo "\n✨ $@ succeeded"
|
||||
endef
|
||||
|
||||
#########
|
||||
@ -82,19 +82,15 @@ X64_TEST_BUILD_DIR := $(BUILD_ROOT)/x64_test
|
||||
X64_TEST_DEBUG_BUILD_DIR := $(BUILD_ROOT)/x64_test_debug
|
||||
WASM_BUILD_DIR := $(BUILD_ROOT)/wasm
|
||||
WASM_DEBUG_BUILD_DIR := $(BUILD_ROOT)/wasm_debug
|
||||
WASM_TEST_BUILD_DIR := $(BUILD_ROOT)/wasm_test
|
||||
WASM_COOLMATH_BUILD_DIR := $(BUILD_ROOT)/wasm_coolmath
|
||||
WASM_ITCH_BUILD_DIR := $(BUILD_ROOT)/wasm_itch
|
||||
WIN32_BUILD_DIR := $(BUILD_ROOT)/win32
|
||||
WINARCADE_BUILD_DIR := $(BUILD_ROOT)/win32_arcade
|
||||
WINDEMO_BUILD_DIR := $(BUILD_ROOT)/win32_demo
|
||||
WIN64_BUILD_DIR := $(BUILD_ROOT)/win64
|
||||
UBUNTU18_BUILD_DIR := $(BUILD_ROOT)/ubuntu18
|
||||
MACOS_BUILD_DIR := $(BUILD_ROOT)/macos
|
||||
BUILD_DIRS := $(X64_BUILD_DIR) $(X64_TEST_BUILD_DIR) $(X64_TEST_DEBUG_BUILD_DIR) $(X64_DEBUG_BUILD_DIR) \
|
||||
$(WASM_BUILD_DIR) $(WASM_DEBUG_BUILD_DIR) $(WASM_TEST_BUILD_DIR) $(WASM_COOLMATH_BUILD_DIR) $(WIN32_BUILD_DIR) \
|
||||
$(WINARCADE_BUILD_DIR) $(WINDEMO_BUILD_DIR) $(WIN64_BUILD_DIR) $(UBUNTU_BUILD_DIR) $(MACOS_BUILD_DIR) \
|
||||
$(WASM_ITCH_BUILD_DIR)
|
||||
$(WASM_BUILD_DIR) $(WASM_DEBUG_BUILD_DIR) $(WASM_COOLMATH_BUILD_DIR) $(WIN32_BUILD_DIR) $(WIN64_BUILD_DIR) \
|
||||
$(UBUNTU_BUILD_DIR) $(MACOS_BUILD_DIR) $(WASM_ITCH_BUILD_DIR)
|
||||
|
||||
$(BUILD_DIRS):
|
||||
mkdir -p $@
|
||||
@ -283,8 +279,8 @@ $(addsuffix /main.o, $(BUILD_DIRS)): $(SRC_DIR)/main.cpp $(addprefix $(SB_SRC_DI
|
||||
|
||||
## Cakefoot test suite ##
|
||||
|
||||
$(addsuffix /test.o, $(BUILD_DIRS)): $(SRC_DIR)/test/test.cpp $(addprefix $(SB_SRC_DIR)/, test/setup.hpp) \
|
||||
| $(BUILD_DIRS)
|
||||
$(addsuffix /test.o, $(BUILD_DIRS)): $(SRC_DIR)/test/test.cpp $(addprefix $(SRC_DIR)/, Cakefoot.hpp) \
|
||||
$(addprefix $(SB_SRC_DIR)/, test/setup.hpp filesystem.hpp) | $(BUILD_DIRS)
|
||||
$(CXX) $< $(CXXFLAGS) -c -o $@
|
||||
|
||||
#############################
|
||||
@ -344,8 +340,14 @@ endif
|
||||
###############
|
||||
|
||||
LINUX_OBJ = glew.o SDL2_rotozoom.o SDL2_gfxPrimitives.o $(SB_O_FILES) $(SRC_O_FILES)
|
||||
LINUX_RESOURCES = resource/ src/shaders/ config/ src/config_steam_demo.json
|
||||
X64_BUILD_LIB_DIR = lib/
|
||||
|
||||
# Extra files to copy to the arcade cabinet build
|
||||
|
||||
LINUX_ARCADE_EXTRA_CONFIG = src/config_arcade.json src/config_arcade_cabinet.json
|
||||
LINUX_ARCADE_EXTRA_SRC = src/cakefoot.service src/update_arcade_cabinet.py
|
||||
|
||||
# EXTRA_PLATFORM_FLAGS refers to extra preprocessor definitions, for example -D__UBUNTU18__, to trigger code blocks
|
||||
# specific to that platform or architecture. These extra flags can be passed in when building using the Linux target on
|
||||
# a specialized platform that needs different behavior in the code (for example, Ubuntu 18, which only supports the
|
||||
@ -357,7 +359,7 @@ X64_BUILD_LIB_DIR = lib/
|
||||
$(NAME)-linux.x64 $(NAME)-linux_debug.x64 $(NAME)-linux_test.x64 $(NAME)-linux_test_debug.x64 : \
|
||||
CFLAGS += $(LINUX_CFLAGS)
|
||||
$(NAME)-linux.x64 $(NAME)-linux_test.x64 : CFLAGS += -O3 $(EXTRA_PLATFORM_FLAGS)
|
||||
$(NAME)-linux_debug.x64 $(NAME)-linux_test_debug.x64 : CFLAGS += -g -O0
|
||||
$(NAME)-linux_debug.x64 $(NAME)-linux_test_debug.x64 : CFLAGS += -g -O0 -D_GLIBCXX_DEBUG
|
||||
$(NAME)-linux.x64 $(NAME)-linux_debug.x64 $(NAME)-linux_test.x64 $(NAME)-linux_test_debug.x64 : \
|
||||
LFLAGS += $(LINUX_LFLAGS)
|
||||
$(NAME)-linux.x64 : LFLAGS += -Wl,-rpath $(X64_BUILD_LIB_DIR)
|
||||
@ -375,12 +377,27 @@ $(NAME)-linux.x64 : $(addprefix $(X64_BUILD_DIR)/, $(LINUX_OBJ))
|
||||
$$(ldd $(X64_BUILD_DIR)/$@ | grep libSDL2_ttf | cut -d" " -f3) \
|
||||
$$(ldd $(X64_BUILD_DIR)/$@ | grep libsteam_api | cut -d" " -f3) \
|
||||
$$(ldd $(X64_BUILD_DIR)/$@ | grep libSDL2_mixer | cut -d" " -f3) ${basename $@}/$(X64_BUILD_LIB_DIR)
|
||||
rsync -arRL resource/ src/shaders/ config.json ${basename $@}
|
||||
rsync -arRL $(LINUX_RESOURCES) ${basename $@}
|
||||
cp $(X64_BUILD_DIR)/$@ ${basename $@}
|
||||
zip -r ${@:x64=zip} ${basename $@}
|
||||
mv ${@:x64=zip} $(X64_BUILD_DIR)
|
||||
mv ${basename $@} /tmp
|
||||
rm -rf /tmp/${basename $@}
|
||||
rm -r /tmp/${basename $@}
|
||||
$(success_message)
|
||||
|
||||
$(NAME)-linux_arcade.x64 : $(NAME)-linux.x64
|
||||
cp $(X64_BUILD_DIR)/$(NAME)-linux.zip .
|
||||
unzip $(NAME)-linux.zip
|
||||
mv $(NAME)-linux ${basename $@}
|
||||
rm ${basename $@}/$(X64_BUILD_LIB_DIR)/libSDL2*
|
||||
cp $(LINUX_ARCADE_EXTRA_CONFIG) ${basename $@}/config
|
||||
cp $(LINUX_ARCADE_EXTRA_SRC) ${basename $@}/src
|
||||
zip -r ${@:x64=zip} ${basename $@}
|
||||
mv ${@:x64=zip} $(X64_BUILD_DIR)
|
||||
mv ${basename $@} /tmp
|
||||
rm -r /tmp/${basename $@}
|
||||
mv $(NAME)-linux.zip /tmp
|
||||
rm -r /tmp/$(NAME)-linux.zip
|
||||
$(success_message)
|
||||
|
||||
# Linux debug-able build, for use with GDB
|
||||
@ -429,7 +446,7 @@ Ubuntu-18 :
|
||||
sudo docker run --name $(DOCKER_NAME) --rm \
|
||||
-v "$(shell pwd)/$(UBUNTU18_BUILD_DIR):$(DOCKER_REMOTE_DIR)/$(X64_BUILD_DIR)" \
|
||||
-v "$(shell pwd)/.git:$(DOCKER_REMOTE_DIR)/.git" \
|
||||
-v "$(shell pwd)/config.json:$(DOCKER_REMOTE_DIR)/config.json" \
|
||||
-v "$(shell pwd)/config:$(DOCKER_REMOTE_DIR)/config" \
|
||||
-v "$(shell pwd)/.gitignore:$(DOCKER_REMOTE_DIR)/.gitignore" \
|
||||
-v "$(shell pwd)/index.html:$(DOCKER_REMOTE_DIR)/index.html" \
|
||||
-v "$(shell pwd)/LICENSE.txt:$(DOCKER_REMOTE_DIR)/LICENSE.txt" \
|
||||
@ -452,54 +469,58 @@ Ubuntu-18 :
|
||||
# Web build #
|
||||
#############
|
||||
|
||||
# Use Emscripten to output JavaScript
|
||||
|
||||
# Use Emscripten to create a WASM build for web browsers
|
||||
EMSCRIPTENHOME = $(HOME)/ext/software/emsdk/upstream/emscripten
|
||||
EMSCRIPTEN_PRELOADS = --preload-file "config.json" --preload-file "resource/" --preload-file "src/shaders/" \
|
||||
--preload-file "src/config_wasm.json"
|
||||
EMSCRIPTEN_GAME_CONFIGS = config.json src/config_wasm.json resource/levels.json
|
||||
|
||||
$(NAME).js $(NAME)_debug.html $(NAME)_test.html $(NAME)_coolmath.js $(NAME)_itch.js : CC = $(EMSCRIPTENHOME)/emcc
|
||||
$(NAME).js $(NAME)_debug.html $(NAME)_test.html $(NAME)_coolmath.js $(NAME)_itch.js : CXX = $(EMSCRIPTENHOME)/em++
|
||||
# Load into the data file for the WASM build
|
||||
EMSCRIPTEN_PRELOADS = --preload-file "config/" --preload-file "resource/" --preload-file "src/shaders/" \
|
||||
--preload-file "src/config_wasm.json@config/config_wasm.json"
|
||||
|
||||
$(NAME).js $(NAME)_test.html : CFLAGS += $(EMSCRIPTEN_CFLAGS)
|
||||
# Trigger a rebuild when these files are edited
|
||||
EMSCRIPTEN_CONFIG_TO_WATCH = config/config.json src/config_wasm.json resource/levels.json
|
||||
|
||||
# Emscripten compilers
|
||||
$(NAME).js $(NAME)_debug.html $(NAME)_coolmath.js $(NAME)_itch.js : CC = $(EMSCRIPTENHOME)/emcc
|
||||
$(NAME).js $(NAME)_debug.html $(NAME)_coolmath.js $(NAME)_itch.js : CXX = $(EMSCRIPTENHOME)/em++
|
||||
|
||||
# Extra compiler flags per Emscripten build
|
||||
$(NAME).js $(NAME)_itch.js : CFLAGS += $(EMSCRIPTEN_CFLAGS)
|
||||
$(NAME)_debug.html : CFLAGS += $(EMSCRIPTEN_CFLAGS) -g2
|
||||
$(NAME)_coolmath.js : CFLAGS = $(EMSCRIPTEN_CFLAGS) -D__COOLMATH__
|
||||
$(NAME)_itch.js : CFLAGS = $(EMSCRIPTEN_CFLAGS) -D__ITCH__
|
||||
|
||||
$(NAME).js $(NAME)_debug.html $(NAME)_test.html $(NAME)_coolmath.js $(NAME)_itch.js : LFLAGS += $(EMSCRIPTEN_LFLAGS)
|
||||
# All WASM builds take the same linker flags
|
||||
$(NAME).js $(NAME)_debug.html $(NAME)_coolmath.js $(NAME)_itch.js : LFLAGS += $(EMSCRIPTEN_LFLAGS)
|
||||
|
||||
$(NAME).js $(NAME)_itch.js $(NAME)_coolmath.js $(NAME)_test.html : EMSCRIPTEN_PRELOADS := $(EMSCRIPTEN_PRELOADS) \
|
||||
--pre-js "src/pre_js.js"
|
||||
# Leave the pre-js out of the debug build
|
||||
$(NAME).js $(NAME)_coolmath.js $(NAME)_itch.js : EMSCRIPTEN_PRELOADS += --pre-js "src/pre_js.js"
|
||||
|
||||
# Build the main WASM build
|
||||
$(NAME).js : $(addprefix $(WASM_BUILD_DIR)/, SDL2_rotozoom.o SDL2_gfxPrimitives.o $(SB_O_FILES) \
|
||||
$(SRC_O_FILES)) $(EMSCRIPTEN_GAME_CONFIGS)
|
||||
$(CXX) $(filter-out $(EMSCRIPTEN_GAME_CONFIGS), $^) $(LFLAGS) $(EMSCRIPTEN_PRELOADS) \
|
||||
$(SRC_O_FILES)) $(EMSCRIPTEN_CONFIG_TO_WATCH)
|
||||
$(CXX) $(filter-out $(EMSCRIPTEN_CONFIG_TO_WATCH), $^) $(LFLAGS) $(EMSCRIPTEN_PRELOADS) \
|
||||
--pre-js "src/pre_js_collect.js" -o $(WASM_BUILD_DIR)/$@
|
||||
$(success_message)
|
||||
|
||||
# Debug build
|
||||
$(NAME)_debug.html : $(addprefix $(WASM_DEBUG_BUILD_DIR)/, SDL2_rotozoom.o SDL2_gfxPrimitives.o $(SB_O_FILES) \
|
||||
$(SRC_O_FILES)) $(EMSCRIPTEN_GAME_CONFIGS)
|
||||
$(CXX) $(filter-out $(EMSCRIPTEN_GAME_CONFIGS), $^) $(LFLAGS) $(EMSCRIPTEN_PRELOADS) \
|
||||
$(SRC_O_FILES)) $(EMSCRIPTEN_CONFIG_TO_WATCH)
|
||||
$(CXX) $(filter-out $(EMSCRIPTEN_CONFIG_TO_WATCH), $^) $(LFLAGS) $(EMSCRIPTEN_PRELOADS) \
|
||||
--memoryprofiler --cpuprofiler -o $(WASM_DEBUG_BUILD_DIR)/$@
|
||||
$(success_message)
|
||||
|
||||
$(NAME)_test.html : $(addprefix $(WASM_TEST_BUILD_DIR)/, SDL2_rotozoom.o SDL2_gfxPrimitives.o $(SB_O_FILES) \
|
||||
$(filter-out main.o, $(SRC_O_FILES)) test.o catch_amalgamated.o) $(EMSCRIPTEN_GAME_CONFIGS)
|
||||
$(CXX) $(filter-out $(EMSCRIPTEN_GAME_CONFIGS), $^) $(LFLAGS) $(EMSCRIPTEN_PRELOADS) \
|
||||
-o $(WASM_TEST_BUILD_DIR)/$@
|
||||
$(success_message)
|
||||
|
||||
# Special builds for embeds at itch.io and Coolmath
|
||||
$(NAME)_coolmath.js $(NAME)_itch.js : JS_SITE = $(patsubst $(NAME)_%.js,%,$@)
|
||||
$(NAME)_coolmath.js $(NAME)_itch.js : EMSCRIPTEN_CONFIG_TO_WATCH += "src/config_$(JS_SITE).json"
|
||||
$(NAME)_coolmath.js : EMSCRIPTEN_PRELOADS += --preload-file "src/config_coolmath.json@config/config_coolmath.json"
|
||||
$(NAME)_itch.js : EMSCRIPTEN_PRELOADS += --preload-file "src/config_itch.json@config/config_itch.json"
|
||||
$(NAME)_coolmath.js $(NAME)_itch.js : BUILD_DIR = $(BUILD_ROOT)/wasm_$(JS_SITE)
|
||||
$(NAME)_coolmath.js : $(addprefix $(WASM_COOLMATH_BUILD_DIR)/, SDL2_rotozoom.o SDL2_gfxPrimitives.o $(SB_O_FILES) \
|
||||
$(SRC_O_FILES)) $(EMSCRIPTEN_GAME_CONFIGS) src/config_coolmath.json src/index_coolmath.html src/embed_style.css
|
||||
$(SRC_O_FILES)) $(EMSCRIPTEN_CONFIG_TO_WATCH) src/index_coolmath.html src/embed_style.css
|
||||
$(NAME)_itch.js : $(addprefix $(WASM_ITCH_BUILD_DIR)/, SDL2_rotozoom.o SDL2_gfxPrimitives.o $(SB_O_FILES) \
|
||||
$(SRC_O_FILES)) $(EMSCRIPTEN_GAME_CONFIGS) src/config_itch.json src/index_itch.html src/embed_style.css
|
||||
$(SRC_O_FILES)) $(EMSCRIPTEN_CONFIG_TO_WATCH) src/index_itch.html src/embed_style.css
|
||||
$(NAME)_coolmath.js $(NAME)_itch.js :
|
||||
$(CXX) $(filter-out $(EMSCRIPTEN_GAME_CONFIGS) src/config_$(JS_SITE).json src/index_$(JS_SITE).html \
|
||||
src/embed_style.css, $^) $(LFLAGS) $(EMSCRIPTEN_PRELOADS) --preload-file "src/config_$(JS_SITE).json" \
|
||||
-o $(BUILD_DIR)/$@
|
||||
$(CXX) $(filter-out $(EMSCRIPTEN_CONFIG_TO_WATCH) src/index_$(JS_SITE).html src/embed_style.css, $^) \
|
||||
$(LFLAGS) $(EMSCRIPTEN_PRELOADS) -o $(BUILD_DIR)/$@
|
||||
cp src/index_$(JS_SITE).html $(BUILD_DIR)/index.html
|
||||
cp src/embed_style.css $(BUILD_DIR)
|
||||
cd $(BUILD_DIR) && zip -r $(NAME)_$(JS_SITE).zip *.js *.wasm *.data *.html *.css && \
|
||||
@ -526,7 +547,7 @@ MACOS_OBJ = $(addprefix $(MACOS_BUILD_DIR)/, glew.o SDL2_rotozoom.o SDL2_gfxPrim
|
||||
MACOS_BUNDLE = $(MACOS_BUILD_DIR)/dmg/$@
|
||||
MACOS_BUNDLE_CONTENTS = $(MACOS_BUILD_DIR)/dmg/$@/Contents
|
||||
MACOS_CROSS_BIN = $(MACOS_CROSS_ROOT)/target/bin
|
||||
MACOS_RESOURCES = resource/ src/shaders/ config.json
|
||||
MACOS_RESOURCES = resource/ src/shaders/ config/ src/config_steam_demo.json
|
||||
MACOS_APP_CONFIG = src/Info.plist
|
||||
MACOS_CERTIFICATE = local/Cakefoot_MacOS_DeveloperID_Application.pem
|
||||
MACOS_RCODESIGN = ~/ext/software/apple-codesign-0.27.0/rcodesign
|
||||
@ -583,10 +604,10 @@ macos-transfer :
|
||||
# the repository. It requires the Android SDK and the source packages for SDL. The paths below should be edited to match
|
||||
# the local system. Icon creation requires Imagemagick's convert tool from <https://imagemagick.org/>.
|
||||
|
||||
SDL_SRC := $(HOME)/ext/software/SDL2-2.26.3
|
||||
SDL_IMAGE_SRC := $(HOME)/ext/software/SDL2_image-2.6.2-android
|
||||
SDL_MIXER_SRC := $(HOME)/ext/software/SDL2_mixer-2.6.2-android
|
||||
SDL_TTF_SRC := $(HOME)/ext/software/SDL2_ttf-2.20.1-android
|
||||
SDL_SRC := $(HOME)/ext/software/SDL2/SDL2-2.26.3
|
||||
SDL_IMAGE_SRC := $(HOME)/ext/software/SDL2/SDL2_image-2.6.2-android
|
||||
SDL_MIXER_SRC := $(HOME)/ext/software/SDL2/SDL2_mixer-2.6.2-android
|
||||
SDL_TTF_SRC := $(HOME)/ext/software/SDL2/SDL2_ttf-2.20.1-android
|
||||
SDL_ANDROID_PROJECT := $(SDL_SRC)/android-project
|
||||
ANDROID_MK := app/jni/src/Android.mk
|
||||
ANDROID_APP_MK := app/jni/Application.mk
|
||||
@ -645,18 +666,19 @@ $(ANDROID_BUILD_DIR)/app-debug.apk: $(ANDROID_BUILD_DIR) $(ANDROID_BUILD_DIR)/$(
|
||||
|
||||
# Set the paths to the directories for the SDL MinGW libraries
|
||||
|
||||
SDL_MINGW_ROOT := $(HOME)/ext/software/SDL2-mingw
|
||||
SDL_MINGW_ROOT := $(HOME)/ext/software/SDL2/SDL2-mingw
|
||||
SDL_MINGW = $(SDL_MINGW_ROOT)/SDL2-2.24.2/$(WIN_ARCH)-w64-mingw32
|
||||
SDL_IMG_MINGW = $(SDL_MINGW_ROOT)/SDL2_image-2.5.2/$(WIN_ARCH)-w64-mingw32
|
||||
SDL_TTF_MINGW = $(SDL_MINGW_ROOT)/SDL2_ttf-2.0.15/$(WIN_ARCH)-w64-mingw32
|
||||
SDL_TTF_MINGW = $(SDL_MINGW_ROOT)/SDL2_ttf-2.20.2/$(WIN_ARCH)-w64-mingw32
|
||||
SDL_MIXER_MINGW = $(SDL_MINGW_ROOT)/SDL2_mixer-2.5.2/$(WIN_ARCH)-w64-mingw32
|
||||
WIN_CONFIGS = config.json
|
||||
WIN_CONFIGS = config/ src/config_steam_demo.json
|
||||
WIN_OBJ = glew.o SDL2_rotozoom.o SDL2_gfxPrimitives.o $(SB_O_FILES) $(SRC_O_FILES)
|
||||
WIN_DLLS = $(SDL_MINGW)/bin/*.dll $(SDL_IMG_MINGW)/bin/*.dll $(SDL_TTF_MINGW)/bin/*.dll $(SDL_MIXER_MINGW)/bin/*.dll \
|
||||
/usr/$(WIN_ARCH)-w64-mingw32/lib/libwinpthread-1.dll
|
||||
|
||||
# If the Steam API is enabled, link to the Steam API library and include the DLL in the distributable, using separate
|
||||
# files for 32-bit and 64-bit builds. The demo and arcade builds do not include the Steam API.
|
||||
# files for 32-bit and 64-bit builds. The demo and arcade distributions do not need the Steam API, but they include it
|
||||
# anyway to avoid compiling two more executables.
|
||||
|
||||
ifeq ($(STEAM),yes)
|
||||
$(NAME)-win32.exe $(NAME)-win64.exe : WIN_CFLAGS += -D__STEAM__ -I$(STEAM_INC)
|
||||
@ -669,38 +691,51 @@ endif
|
||||
# Set the compiler to the MinGW compilers
|
||||
|
||||
$(NAME)-win64.exe : WIN_ARCH = x86_64
|
||||
$(NAME)-win32.exe $(NAME)-win32-ARCADE_ONLY.exe $(NAME)-win32-DEMO.exe : WIN_ARCH = i686
|
||||
$(NAME)-win32.exe : WIN_ARCH = i686
|
||||
$(NAME)-win64.exe : CC = $(WIN_ARCH)-w64-mingw32-gcc-posix
|
||||
$(NAME)-win64.exe : CXX = $(WIN_ARCH)-w64-mingw32-g++-posix
|
||||
$(NAME)-win32.exe $(NAME)-win32-ARCADE_ONLY.exe $(NAME)-win32-DEMO.exe : CC = $(WIN_ARCH)-w64-mingw32-gcc-posix
|
||||
$(NAME)-win32.exe $(NAME)-win32-ARCADE_ONLY.exe $(NAME)-win32-DEMO.exe : CXX = $(WIN_ARCH)-w64-mingw32-g++-posix
|
||||
$(NAME)-win64.exe $(NAME)-win32.exe $(NAME)-win32-ARCADE_ONLY.exe $(NAME)-win32-DEMO.exe : CFLAGS += $(WIN_CFLAGS)
|
||||
$(NAME)-win32-ARCADE_ONLY.exe : CFLAGS += -D__ARCADE_ONLY__
|
||||
$(NAME)-win32-DEMO.exe : CFLAGS += -D__DEMO__
|
||||
$(NAME)-win32.exe $(NAME)-win64.exe $(NAME)-win32-ARCADE_ONLY.exe $(NAME)-win32-DEMO.exe : LFLAGS += $(WIN_LFLAGS)
|
||||
$(NAME)-win32.exe : CC = $(WIN_ARCH)-w64-mingw32-gcc-posix
|
||||
$(NAME)-win32.exe : CXX = $(WIN_ARCH)-w64-mingw32-g++-posix
|
||||
$(NAME)-win32.exe $(NAME)-win64.exe : CFLAGS += $(WIN_CFLAGS)
|
||||
$(NAME)-win32.exe $(NAME)-win64.exe : LFLAGS += $(WIN_LFLAGS)
|
||||
$(NAME)-win64.exe : WIN_BUILD_DIR := $(WIN64_BUILD_DIR)
|
||||
$(NAME)-win32.exe : WIN_BUILD_DIR := $(WIN32_BUILD_DIR)
|
||||
$(NAME)-win32-ARCADE_ONLY.exe : WIN_BUILD_DIR := $(WINARCADE_BUILD_DIR)
|
||||
$(NAME)-win32-DEMO.exe : WIN_BUILD_DIR := $(WINDEMO_BUILD_DIR)
|
||||
$(NAME)-win32-ARCADE_ONLY.exe : WIN_CONFIGS += src/config_arcade.json
|
||||
$(NAME)-win32-DEMO.exe : WIN_CONFIGS += src/config_demo.json
|
||||
|
||||
# In the following rules, it doesn't seem possible to use WIN_BUILD_DIR in addprefix for all targets, not sure why
|
||||
|
||||
$(NAME)-win64.exe : $(addprefix $(WIN64_BUILD_DIR)/, $(WIN_OBJ)) $(WIN_CONFIGS)
|
||||
$(NAME)-win32.exe : $(addprefix $(WIN32_BUILD_DIR)/, $(WIN_OBJ)) $(WIN_CONFIGS)
|
||||
$(NAME)-win32-ARCADE_ONLY.exe: $(addprefix $(WINARCADE_BUILD_DIR)/, $(WIN_OBJ)) $(WIN_CONFIGS)
|
||||
$(NAME)-win32-DEMO.exe: $(addprefix $(WINDEMO_BUILD_DIR)/, $(WIN_OBJ)) $(WIN_CONFIGS)
|
||||
$(NAME)-win64.exe $(NAME)-win32.exe $(NAME)-win32-ARCADE_ONLY.exe $(NAME)-win32-DEMO.exe :
|
||||
$(NAME)-win64.exe $(NAME)-win32.exe :
|
||||
$(CXX) $(filter-out $(WIN_CONFIGS), $^) $(LFLAGS) -o $(WIN_BUILD_DIR)/$@
|
||||
mkdir ${basename $@}
|
||||
cp $(WIN_DLLS) ${basename $@}
|
||||
rsync -arRL resource/ src/shaders/ $(WIN_CONFIGS) ${basename $@}
|
||||
|
||||
# Create a distributable ZIP
|
||||
|
||||
cp $(WIN_BUILD_DIR)/$@ ${basename $@}
|
||||
zip -r ${@:exe=zip} ${basename $@}
|
||||
mv ${@:exe=zip} $(WIN_BUILD_DIR)
|
||||
|
||||
# Create a distributable ZIP for arcade cabinets, pre-configured to be arcade-only. Remove the Steam demo config because
|
||||
# it is not used.
|
||||
|
||||
cp src/config_arcade.json ${basename $@}/config/
|
||||
rm ${basename $@}/src/config_steam_demo.json
|
||||
zip -r ${@:.exe=-ARCADE.zip} ${basename $@}
|
||||
mv ${@:.exe=-ARCADE.zip} $(WIN_BUILD_DIR)
|
||||
rm ${basename $@}/config/config_arcade.json
|
||||
|
||||
# Create a distributable ZIP for event demos, pre-configured to reset automatically
|
||||
|
||||
cp src/config_event_demo.json ${basename $@}/config/
|
||||
zip -r ${@:.exe=-EVENT_DEMO.zip} ${basename $@}
|
||||
mv ${@:.exe=-EVENT_DEMO.zip} $(WIN_BUILD_DIR)
|
||||
|
||||
# Delete folder used to make ZIP
|
||||
|
||||
mv ${basename $@} /tmp
|
||||
rm -rf /tmp/${basename $@}
|
||||
mv ${@:exe=zip} $(WIN_BUILD_DIR)
|
||||
$(success_message)
|
||||
|
||||
# Transfer the Windows builds to a Windows computer and unzip the builds automatically. This requires OpenSSH to be
|
||||
@ -712,7 +747,8 @@ WIN_USER = ohsqueezy
|
||||
WIN_DEST = /Users/$(WIN_USER)/Desktop
|
||||
|
||||
win-transfer :
|
||||
scp -P $(WIN_PORT) $(WIN64_BUILD_DIR)/*.zip $(WIN32_BUILD_DIR)/*.zip "$(WIN_USER)@$(WIN_HOST):$(WIN_DEST)"
|
||||
scp -P $(WIN_PORT) $(WIN64_BUILD_DIR)/*win64.zip $(WIN32_BUILD_DIR)/*win32.zip \
|
||||
"$(WIN_USER)@$(WIN_HOST):$(WIN_DEST)"
|
||||
ssh -p $(WIN_PORT) $(WIN_USER)@$(WIN_HOST) -x \
|
||||
"cd $(WIN_DEST) && (for %I in (*.zip) do tar.exe -xf %I) && del *.zip"
|
||||
|
||||
@ -742,7 +778,7 @@ external : $(NAME).app Ubuntu-18
|
||||
|
||||
# Builds meant for running unit tests
|
||||
|
||||
local-tests : $(NAME)-linux_test.x64 $(NAME)_test.html
|
||||
local-tests : $(NAME)-linux_test.x64
|
||||
|
||||
# Debug builds
|
||||
|
||||
|
98
Press_Kit.md
@ -14,14 +14,16 @@
|
||||
| | [Cakefoot on IGDB](https://www.igdb.com/games/cakefoot) |
|
||||
| Genre | Action, Indie, Arcade, Rage, Minimalist, Single-button, Masocore |
|
||||
| Platforms | Web, PC, Mac, Linux, [Coolmath](https://coolmathgames.com) |
|
||||
| Release | 📅 1/1/2024 (Early Access) |
|
||||
| | 📅 5/10/2024 (Web, PC, Mac, Linux) |
|
||||
| | 📅 TBA (Android) |
|
||||
| Price | Free-to-play, Ad-supported (Web) |
|
||||
| | $2.99 (Downloadable) |
|
||||
| Hashtags | #freetoplay #ragegame #browsergame #webgame #arcade #steam #steamdeck |
|
||||
| Release | 📅 1/1/2024 - Early Access |
|
||||
| | 📅 5/10/2024 - Full Game |
|
||||
| | 📅 12/22/2024 - Version 1.4 |
|
||||
| | 📅 TBA - Android |
|
||||
| Price | Free-to-play, Ad-supported on the web |
|
||||
| | $2.99 on PC, MacOS, and Linux (with free demo!) |
|
||||
| E-mail | 📧 cocktail.frank at dank.game |
|
||||
|
||||
Fork it up! _Cakefoot_ is a challenging, single-button dodge 'em up on rails. Pilot a walking cake -- hold to accelerate, let go to drift back -- and survive against all odds. Featuring 22 epic levels, arcade mode, unlockables, and high scores, _Cakefoot_ is endlessly replayable, and impossible to defeat.
|
||||
Fork it up! _Cakefoot_ is a challenging, single-button dodge 'em up on rails. Pilot a walking cake -- hold to accelerate, let go to drift back -- and survive against all odds. Featuring 22 epic levels, 30+ achievements, arcade mode, unlockables, and high scores, _Cakefoot_ is endlessly replayable, and impossible to defeat.
|
||||
|
||||
Features
|
||||
========
|
||||
@ -29,8 +31,14 @@ Features
|
||||
* Unique game play mechanic: dodge 'em up on rails
|
||||
* 22-level Quest Mode
|
||||
* Arcade mode
|
||||
* Modular enemy types that combine into endless patterns
|
||||
* 30+ achievements
|
||||
* Enemy types that combine into endless patterns
|
||||
* Progress saves automatically
|
||||
* Free to play on the web and free demo on Steam!
|
||||
|
||||
More Features
|
||||
=============
|
||||
|
||||
* Unlock new difficulty levels and visual effects by collecting challenge coins
|
||||
* Runs on Windows, Mac, Linux, and in web browsers at 60 FPS in any resolution
|
||||
* Supports mouse, keyboard, and gamepad
|
||||
@ -39,18 +47,24 @@ Features
|
||||
* Simple JSON syntax for creating and sharing custom levels and skins
|
||||
* Modifiable [source code](https://open.shampoo.ooo/shampoo/cakefoot)
|
||||
|
||||
Trailer
|
||||
=======
|
||||
Trailers
|
||||
========
|
||||
|
||||
* [YouTube](https://youtu.be/xn-iNcUlIpo)
|
||||
* [Twitter](https://x.com/diskmem/status/1769043497267449956)
|
||||
* [Direct Download](https://5.shampoo.ooo/video/Cakefoot_Release_trailer.mp4)
|
||||
### Achievements Trailer ###
|
||||
|
||||
<iframe src="https://www.youtube.com/embed/xn-iNcUlIpo?si=vIXnaLxESoFKEi-p" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
<iframe src="https://www.youtube.com/embed/PRB-pDrbZTk?si=yUaS7XTgzNugI-jm" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
|
||||
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">🔥get baked🔥 by this cake game 😡 <a href="https://twitter.com/hashtag/screenshotsaturday?src=hash&ref_src=twsrc%5Etfw">#screenshotsaturday</a> <a href="https://t.co/MdUJVWRxcM">pic.twitter.com/MdUJVWRxcM</a></p>— ron🤡 mac🍔 don👔 (@diskmem) <a href="https://twitter.com/diskmem/status/1769043497267449956?ref_src=twsrc%5Etfw">March 16, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Mary Christmas my little pogchamp 🐑🎄 <a href="https://t.co/z24FPlG3ND">pic.twitter.com/z24FPlG3ND</a></p>— 𝓬𝓸𝓬𝓴𝓽𝓪𝓲𝓵 𝓯𝓻𝓪𝓷𝓴 🍹 (@diskmem) <a href="https://twitter.com/diskmem/status/1871647905649693028?ref_src=twsrc%5Etfw">December 24, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||
|
||||
<video src="https://5.shampoo.ooo/video/Cakefoot_Release_trailer.mp4" controls></video>
|
||||
* [Achievements Trailer on YouTube - Landscape orientation](https://youtu.be/PRB-pDrbZTk)
|
||||
* [Achievements Trailer on YouTube Shorts - Portrait orientation](https://youtube.com/shorts/77jZuW3j4PQ)
|
||||
* [Achievements Trailer on X/Twitter](https://x.com/diskmem/status/1871647905649693028)
|
||||
|
||||
### Release Trailer ###
|
||||
|
||||
* [Release Trailer on YouTube](https://youtu.be/xn-iNcUlIpo)
|
||||
* [Release Trailer on X/Twitter](https://x.com/diskmem/status/1769043497267449956)
|
||||
* [Release Trailer Direct Download](https://5.shampoo.ooo/video/Cakefoot_Release_trailer.mp4)
|
||||
|
||||
Description
|
||||
===========
|
||||
@ -110,6 +124,16 @@ Gameplay video
|
||||
News
|
||||
====
|
||||
|
||||
### 2025 ###
|
||||
|
||||
News items for the new year!
|
||||
|
||||
* Cakefoot version 1.4 is the largest update yet - 30+ achievements, detailed stat tracking, and new music have been added on all platforms.
|
||||
|
||||
* Cakefoot will be free to play at [MAGFest](https://super.magfest.org) Indie Arcade 2025 from Jan. 22 - 25.
|
||||
|
||||
* A new Cakefoot cabinet for an arcade opening in San Francisco, CA has begun production, featuring custom artwork by dank.game and @doctor_begonia.
|
||||
|
||||
### Here comes Cakefoot 😱 ###
|
||||
|
||||
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Wake up Neo, new walk into traffic simulator just dropped! 💊😎 Cakefoot is out on Steam + Coolmath games + Itch 🗓today🗓 <a href="https://twitter.com/hashtag/indiedev?src=hash&ref_src=twsrc%5Etfw">#indiedev</a> <a href="https://t.co/WmY84XcoAp">pic.twitter.com/WmY84XcoAp</a></p>— 𝓬𝓸𝓬𝓴𝓽𝓪𝓲𝓵 𝓯𝓻𝓪𝓷𝓴 🍹 (@diskmem) <a href="https://twitter.com/diskmem/status/1789040321021542476?ref_src=twsrc%5Etfw">May 10, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||
@ -147,24 +171,34 @@ The [Steam store page is live](https://store.steampowered.com/app/2869020/Cakefo
|
||||
Events
|
||||
======
|
||||
|
||||
Upcoming conventions, festivals, and events
|
||||
Conventions, festivals, and events which have featured Cakefoot
|
||||
|
||||
* [1 year anniversary of Boshi's Place party](https://www.instagram.com/p/C40yS-cOp4e/) (Sat. 4/6 @ 7pm, New York, NY) - Cakefoot will be playable
|
||||
| Event | Date | Location |
|
||||
| :------------------------------------- | :--------------- | :------------------- |
|
||||
| [MAGFest 2025][] | Jan. 23-26, 2025 | National Harbor, MD |
|
||||
| [Playcrafting Indie Dev Night][] | Dec. 12, 2024 | Brooklyn, NY |
|
||||
| [Shenanicon][] | Jun. 22-23, 2024 | Parsippany, NJ |
|
||||
| [Boshi's Place One Year Anniversary][] | Apr. 6, 2024 | Brooklyn, NY |
|
||||
| [AwesomeCon][] | Mar. 8-10, 2024 | Washington D.C. |
|
||||
| [Playcrafting Mini Expo][] | Feb. 12, 2024 | New York, NY |
|
||||
| [DerpyCon][] | Oct. 26-29, 2024 | New Brunswick, NJ |
|
||||
|
||||
Past events
|
||||
|
||||
* [Awesome Con](https://awesome-con.com/) (Fri. 3/8 - Sun. 3/10, Washington D.C.)
|
||||
* [Playcrafting Mini Expo](https://www.eventbrite.com/e/playcrafting-mini-expo-february-2024-microsoft-tickets-789293256377) (Mon. 2/12, New York, NY)
|
||||
* [Derpy Con](https://derpycon.com/) (Thu. 10/26 - Sun. 10/29, New Bruswick, NJ)
|
||||
[MAGFest 2025]: https://super.magfest.org
|
||||
[Playcrafting Indie Dev Night]: https://www.eventbrite.com/e/nyc-indie-dev-night-tickets-858271421927
|
||||
[Shenanicon]: https://shenanicon.com/
|
||||
[Boshi's Place One Year Anniversary]: https://www.instagram.com/p/C40yS-cOp4e/
|
||||
[AwesomeCon]: https://awesome-con.com/
|
||||
[Playcrafting Mini Expo]: https://www.eventbrite.com/e/playcrafting-mini-expo-february-2024-microsoft-tickets-789293256377
|
||||
[DerpyCon]: https://derpycon.com/
|
||||
|
||||
About
|
||||
=====
|
||||
|
||||
The original version of _Cakefoot_ was a free downloadable game created in 2019 for [Ludum Dare 45](https://ldjam.com/events/ludum-dare/45/cakewalk) by [diskmem](https://ohsqueezy.itch.io) and [Azuria Sky](https://azuria-sky.bandcamp.com). Like the final version, the original was also a single-button arcade game with a unique, 2D physics on-rails movement mechanic. A small update after the jam added enemy sprites and a new level. The game remained available for free on itch.io. In 2023, after _Cakefoot_ was successfully pitched as a web game to [Coolmathgames](https://coolmathgames.com), diskmem revived the project and added new features, including introductory levels, coins, automatic saves, arcade mode, and unlockables.
|
||||
The original version of _Cakefoot_ was a free downloadable game created in 2019 for [Ludum Dare 45](https://ldjam.com/events/ludum-dare/45/cakewalk) by [diskmem](https://ohsqueezy.itch.io) and [Azuria Sky](https://azuria-sky.bandcamp.com). The original was also a single-button dodge 'em up with 2D on-rails movement. An update after the jam added enemy sprites and a new level. The game remained available for free on itch.io. In 2023, after _Cakefoot_ was successfully pitched as a web game to [Coolmathgames](https://coolmathgames.com), the project was revived with new features, including introductory levels, coins, automatic saves, arcade mode, and unlockables.
|
||||
|
||||
Arcade mode was added when _Cakefoot_ was exhibited at [Derpy Con](https://derpycon.com) in 2023 as part of the Barnyardia pop-up arcade by diskmem and [snakesandrews](https://twitter.com/snakesandrews). Arcade mode gives the player limited time and records the farthest distance reached before time runs out. High scores are saved and displayed in the game. After the convention, the arcade cabinet returned with _Cakefoot_ to its owners at [Wonderville](https://wonderville.nyc) in Brooklyn, NY, where it is still available to play.
|
||||
Arcade mode was added when _Cakefoot_ was exhibited at [Derpy Con](https://derpycon.com) in 2023 as part of the Barnyardia pop-up arcade by diskmem and [snakesandrews](https://twitter.com/snakesandrews). Arcade mode gives the player limited time and records the farthest distance reached before time runs out. High scores are saved and displayed in the game. After the convention, the arcade cabinet returned with _Cakefoot_ to [Wonderville](https://wonderville.nyc) in Brooklyn, NY, where it is still available to play.
|
||||
|
||||
A custom game engine called [SPACE🪐BOX](https://open.shampoo.ooo/shampoo/spacebox) is being developed alongside the game. One of the main features of the engine is it supports exporting to many platforms, including PC, Mac, Linux, Raspberry Pi, and web browsers. _Cakefoot_ is playable at [💫dank.game💫](https://dank.game), a platform created for _Cakefoot_ and other upcoming games built with the engine.
|
||||
A custom game engine called [SPACE🪐BOX](https://open.shampoo.ooo/shampoo/spacebox) is being developed alongside the game. One of the main features of the engine is it supports exporting to many platforms, including PC, Mac, Linux, Raspberry Pi, and web browsers. _Cakefoot_ is playable at [💫dank.game💫](https://dank.game), a platform created for _Cakefoot_ and future games built with the engine.
|
||||
|
||||
Logo
|
||||
====
|
||||
@ -173,6 +207,8 @@ Logo
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Streaming
|
||||
=========
|
||||
@ -182,13 +218,13 @@ Permission is granted to stream the contents of _Cakefoot_ for any commercial or
|
||||
About dank.game
|
||||
===============
|
||||
|
||||
💫dank.game💫 is a portal for cross-platform ad-supported games primarily created for web browsers. The first game on the portal is _Cakefoot_, designed by dank.game and released in 2024. Further game development is supported by advertising, selling platform-specific builds, and offering subscription tiers, starting at 🆓
|
||||
💫dank.game💫 is a portal for cross-platform ad-supported games primarily created for web browsers. The first game on the portal is _Cakefoot_, designed by dank.game and released in 2024. Development is supported by in-game ads, platform-specific downloads, and subscription tiers, starting at 🆓
|
||||
|
||||
Contact
|
||||
=======
|
||||
|
||||
| Method | Contact information |
|
||||
| :------ | :-------------------------- |
|
||||
| E-mail | cocktail.frank at dank.game |
|
||||
| Web | <https://dank.game> |
|
||||
| X | <https://x.com/diskmem> |
|
||||
| Method | Contact information |
|
||||
| :-------------- | :-------------------------- |
|
||||
| E-mail | cocktail.frank at dank.game |
|
||||
| Web | <https://dank.game> |
|
||||
| X/Twitter | <https://x.com/diskmem> |
|
||||
|
87
README.md
@ -1,4 +1,4 @@
|
||||

|
||||
<img src="doc/Cakefoot_logo.png" width=700 />
|
||||
|
||||
Fork it up! _Cakefoot_ is a challenging, single-button dodge ’em up on rails. Pilot a walking cake – hold to accelerate, let go to drift back – and survive against all odds. Featuring 22 epic levels, arcade mode, unlockables, and high scores, _Cakefoot_ is endlessly replayable, and impossible to defeat.
|
||||
|
||||
@ -6,18 +6,24 @@ Fork it up! _Cakefoot_ is a challenging, single-button dodge ’em up on rails.
|
||||
|
||||
_Cakefoot_ is one of the first games made with the open-source [SPACE🪐BOX](https://open.shampoo.ooo/shampoo/spacebox) engine and is free to [play online](https://cakefoot.dank.game). It is the first game available for the [dank.game](https://dank.game) web games portal.
|
||||
|
||||
Mod
|
||||
===
|
||||
Modding
|
||||
=======
|
||||
|
||||
It is possible to edit or add levels, sprites, and some design parameters without needing to rebuild the game.
|
||||
The source code is not necessary for changing some parts of the game. For example, levels can be added or edited by editing the file `resource/levels.json`. Graphics and sound can be changed by editing texture file paths in `config.json`.
|
||||
|
||||
Since modding is not well documented yet, it is recommended to copy the entire build to another folder in case any save files get corrupted when running the mod. Additionally, any files modified should be saved to a backup for easy recovery.
|
||||
|
||||
### Config and assets ###
|
||||
|
||||
Asset swaps and some style edits, such as colors and fonts, are the easiest mods to do. Change the relevant values in [config.json](config.json). In some cases, the values can even be changed while the game is running just by saving the file changes.
|
||||
Asset swaps, such as textures and sound, and some style edits, such as colors and fonts, are the easiest mods to do. Change the relevant values in [config.json](config.json). In some cases, the values can even be changed while the game is running just by saving the file changes.
|
||||
|
||||
### Levels ###
|
||||
|
||||
Levels in [levels.json](resource/levels.json) are also easy to add and mod using JSON, but the syntax is not documented yet. The shape of the curve, checkpoints, and enemies are all definable in the level syntax.
|
||||
Levels in [levels.json](resource/levels.json) are also easy to add and mod using JSON, but the syntax is not documented yet. The shape of the curve, checkpoints, and enemies are all definable in the level syntax. Make sure to make a backup of the original file if modifying it.
|
||||
|
||||
### Shaders ###
|
||||
|
||||
There are simple shader files in `src/shaders/gl/`. These can be edited to make graphics effects without recompiling the game.
|
||||
|
||||
### Code ###
|
||||
|
||||
@ -32,11 +38,31 @@ Clone both the game code and the [SPACE🪐BOX](https://open.shampoo.ooo/shampoo
|
||||
|
||||
Each platform has a corresponding target in the [Makefile](Makefile). There is some documentation in the Makefile itself that should be checked. The current supported platforms are web browsers, Linux, Windows, and MacOS.
|
||||
|
||||
Until further documentation is added, one way to get started building for a platform is to first build the [box demo](https://open.shampoo.ooo/shampoo/spacebox/src/branch/main/demo/box) for the desired platform in a separate project.
|
||||
Until further documentation is added, one way to get started building for a platform is to first build the [box demo][3] for the desired platform in a separate project. This will ensure the dependencies for the game engine are installed.
|
||||
|
||||
Once the box demo is confirmed to build, try making a build of Cakefoot, for example the Linux version
|
||||
Once the box demo is confirmed to build, try making a build of Cakefoot. For example, the Linux version can be built with the following command.
|
||||
|
||||
$ make Cakefoot-linux_debug.x86_64
|
||||
$ make Cakefoot-linux.x64
|
||||
|
||||
[3]: https://open.shampoo.ooo/shampoo/spacebox/src/branch/main/demo/box
|
||||
|
||||
Arcade cabinet
|
||||
==============
|
||||
|
||||
Cakefoot can be configured to run in arcade-only mode. In this mode, there is just a play button on the title screen and no options. When the button is pressed, arcade mode runs. The player plays one round of arcade, adds their name to the high scores list, and returns to the title screen.
|
||||
|
||||
An arcade cabinet or custom installation can be built to power-on, launch, and run the game continuously, using just a power button, if the computer Cakefoot is installed on is configured to power on and load the game automatically. There are options specific to the arcade version, including switching between free play and credits for commercial use. On Linux, it is also possible to set up automatic updates over WiFi.
|
||||
|
||||
### Installation ###
|
||||
|
||||
#### *See [Installing Cakefoot as an Arcade Cabinet][8]*
|
||||
|
||||
### Operator's Manual ###
|
||||
|
||||
#### *See [Operator's Manual][9]*
|
||||
|
||||
[8]: doc/Installing_Cakefoot_as_an_Arcade_Cabinet.md
|
||||
[9]: doc/Cakefoot_Arcade_Operators_Manual.md
|
||||
|
||||
Press kit
|
||||
=========
|
||||
@ -57,7 +83,39 @@ See [LICENSE.txt](LICENSE.txt) for details, including how to provide credit.
|
||||
Releases
|
||||
========
|
||||
|
||||
1.2.0
|
||||
Release versions are available to checkout using [tags][]. This project uses [semantic versioning](https://semver.org).
|
||||
|
||||
[1.4.2][]
|
||||
|
||||
- Add an arcade operator's menu for configuring options applicable to the Cakefoot arcade cabinet
|
||||
- Add a credits system with configurable credit requirements to the arcade-only mode
|
||||
- Use new config search path feature to include config specific to web, arcade, and demo builds
|
||||
|
||||
[1.4.1][]
|
||||
|
||||
- Add idle timeout to arcade cabinet build
|
||||
|
||||
[1.4.0][]
|
||||
|
||||
- Draw pop-up when achievement is unlocked
|
||||
- Sync all stats and unlocked achievements to Steam when game loads
|
||||
- Fix achievements and stats menus so they load new data when opened
|
||||
|
||||
[1.3.1][]
|
||||
|
||||
- Display stats and achievements in sub-menus on the title screen.
|
||||
- Fix scoreboard text wrapping using upstream text wrapping improvements.
|
||||
|
||||
[1.3.0][]
|
||||
|
||||
- New background music: each of the four chapters has a new medley of songs
|
||||
- Positional audio for sound effects based on where the character is on the screen
|
||||
|
||||
[1.2.1][]
|
||||
|
||||
- Fix bug causing walk backward achievements to trigger prematurely.
|
||||
|
||||
[1.2.0][]
|
||||
|
||||
- Add achievements and stats, with support for syncing to Steam.
|
||||
- Store player's fullscreen and sound preferences, and re-apply them when opening the game.
|
||||
@ -101,6 +159,15 @@ Releases
|
||||
- bug fix: prevent character from moving when level loads or play is resumed from the pause menu
|
||||
- bug fix: cancel character walking sfx when paused
|
||||
|
||||
[tags]: https://open.shampoo.ooo/shampoo/cakefoot/tags
|
||||
[1.4.2]: https://open.shampoo.ooo/shampoo/cakefoot/releases/tag/1.4.2
|
||||
[1.4.1]: https://open.shampoo.ooo/shampoo/cakefoot/releases/tag/1.4.1
|
||||
[1.4.0]: https://open.shampoo.ooo/shampoo/cakefoot/releases/tag/1.4.0
|
||||
[1.3.1]: https://open.shampoo.ooo/shampoo/cakefoot/releases/tag/1.3.1
|
||||
[1.3.0]: https://open.shampoo.ooo/shampoo/cakefoot/releases/tag/1.3.0
|
||||
[1.2.1]: https://open.shampoo.ooo/shampoo/cakefoot/releases/tag/1.2.1
|
||||
[1.2.0]: https://open.shampoo.ooo/shampoo/cakefoot/releases/tag/1.2.0
|
||||
|
||||
Contact
|
||||
=======
|
||||
|
||||
|
@ -27,8 +27,6 @@
|
||||
"level hud visible": false,
|
||||
"hitbox": false,
|
||||
"use play button": false,
|
||||
"arcade only": false,
|
||||
"use arcade prompt": false,
|
||||
"game over text": "GAME OVER",
|
||||
"game over display time": 2.5,
|
||||
"game over foreground": [255.0, 255.0, 255.0, 255.0],
|
||||
@ -53,26 +51,13 @@
|
||||
"scoreboard scale": [1.4, 0.15],
|
||||
"scoreboard translation": [-0.4, 0.835],
|
||||
"scoreboard scale": [1.35, 0.14],
|
||||
"scoreboard wrap": 3000,
|
||||
"scoreboard wrap": 0,
|
||||
"qr display": false,
|
||||
"qr background display": true,
|
||||
"qr background texture": "resource/qr_background.png",
|
||||
"qr texture": "resource/qr.png",
|
||||
"qr translation": [1.25, -0.465],
|
||||
"qr scale": [0.525, 0.525],
|
||||
"social texture": "resource/Social_media_buttons.png",
|
||||
"social arcade translation": [-1.35, -0.82],
|
||||
"social qr translation": [-1.32, -0.55],
|
||||
"social web translation": [1.42, -0.82],
|
||||
"social scale": [0.35, 0.11],
|
||||
"social diskmem texture": "resource/Social_media_diskmem.png",
|
||||
"social azuria sky texture": "resource/Social_media_azuria_sky.png",
|
||||
"social single scale": 0.35,
|
||||
"social single ratio": 0.16333333,
|
||||
"social diskmem translation": [1.42, -0.75],
|
||||
"social azuria sky translation": [1.42, -0.87],
|
||||
"social diskmem url": "https://twitter.com/diskmem",
|
||||
"social azuria sky url": "https://twitter.com/azuria_sky",
|
||||
"end screen timeout": 40.0,
|
||||
"enemy sprite scale": 0.024691358,
|
||||
"quest best text": "★ ",
|
||||
@ -95,8 +80,6 @@
|
||||
"arcade warning color": [0.5, 0.0, 0.0, 1.0],
|
||||
"auto save translation": [-1.45, -0.65],
|
||||
"auto save scale": [0.325, 0.15],
|
||||
"social media click": false,
|
||||
"social media visible": false,
|
||||
"highlight saturation": 1.0,
|
||||
"highlight value": 0.5,
|
||||
"loot offset": [0.0, 0.1],
|
||||
@ -125,10 +108,8 @@
|
||||
|
||||
"configuration":
|
||||
{
|
||||
"auto refresh": true,
|
||||
"auto refresh interval": 1.0,
|
||||
"wasm config path": "src/config_wasm.json",
|
||||
"android config path": "src/config_android.json"
|
||||
"auto refresh": false,
|
||||
"auto refresh interval": 1.0
|
||||
},
|
||||
|
||||
"recording":
|
||||
@ -145,7 +126,7 @@
|
||||
"input":
|
||||
{
|
||||
"suppress any key on mods": true,
|
||||
"any key ignore commands": ["left", "right", "up", "down", "pause"],
|
||||
"any key ignore commands": ["left", "right", "up", "down", "pause", "add credit", "operator"],
|
||||
"gamepad pause button index": 9,
|
||||
"gamepad axis cooldown": 0.2,
|
||||
"gamepad reset button index": 8,
|
||||
@ -165,7 +146,9 @@
|
||||
"skip forward": [],
|
||||
"skip backward": [],
|
||||
"memory": [],
|
||||
"coords": []
|
||||
"coords": [],
|
||||
"add credit": "f12",
|
||||
"operator": "f7"
|
||||
},
|
||||
|
||||
"log":
|
||||
@ -188,11 +171,6 @@
|
||||
"path": "resource/BPmono.ttf",
|
||||
"size": 24
|
||||
},
|
||||
"small":
|
||||
{
|
||||
"path": "resource/BPmono.ttf",
|
||||
"size": 14
|
||||
},
|
||||
"large":
|
||||
{
|
||||
"path": "resource/BPmono.ttf",
|
||||
@ -207,6 +185,16 @@
|
||||
{
|
||||
"path": "resource/DejaVuSans.ttf",
|
||||
"size": 44
|
||||
},
|
||||
"narrow medium":
|
||||
{
|
||||
"path": "resource/PT_Sans_Narrow.ttf",
|
||||
"size": 19
|
||||
},
|
||||
"narrow progress menu":
|
||||
{
|
||||
"path": "resource/PT_Sans_Narrow.ttf",
|
||||
"size": 20
|
||||
}
|
||||
},
|
||||
|
||||
@ -218,7 +206,9 @@
|
||||
"demo message": "resource/Demo_message.png",
|
||||
"coin missing": "resource/coin_missing.png",
|
||||
"steam button": "resource/steam_button.png",
|
||||
"dank logo": "resource/dank_logo.png"
|
||||
"dank logo": "resource/dank_logo.png",
|
||||
"check": "resource/check_mark.png",
|
||||
"x": "resource/unchecked.png"
|
||||
},
|
||||
|
||||
"curve":
|
||||
@ -296,7 +286,6 @@
|
||||
"text background": [60.0, 60.0, 60.0, 255.0],
|
||||
"start text": "✶✶ PLAY ✶✶",
|
||||
"start translation": [0.0, -0.4],
|
||||
"start alt texture": "resource/press_button_to_start.png",
|
||||
"start alt translation": [0.0, -0.5],
|
||||
"start alt scale": [0.5787, 0.91],
|
||||
"resume text": "RESUME",
|
||||
@ -366,7 +355,7 @@
|
||||
"fullscreen text text": "FULLSCREEN",
|
||||
"fullscreen text translation home": [0.0, -0.85],
|
||||
"fullscreen text translation pause": [0.0, -0.16],
|
||||
"fullscreen text dimensions": [850.0, 40.0],
|
||||
"fullscreen text dimensions": [450.0, 22.0],
|
||||
"fullscreen text scale": 0.71,
|
||||
"bgm text on": "BGM ON",
|
||||
"bgm text off": "BGM OFF",
|
||||
@ -383,7 +372,17 @@
|
||||
"steam ratio": 0.1890831252,
|
||||
"dank logo translation": [1.45, -0.65],
|
||||
"dank logo scale": 0.25,
|
||||
"dank logo ratio": 0.188406
|
||||
"dank logo ratio": 0.188406,
|
||||
"arcade prompt":
|
||||
{
|
||||
"font": "large",
|
||||
"foreground": [0.0, 0.0, 0.0, 255.0],
|
||||
"background": [160.0, 160.0, 160.0, 255.0],
|
||||
"dimensions": [900.0, 180.0],
|
||||
"translation": [0.0, -0.5],
|
||||
"scale": 0.8,
|
||||
"ratio": 0.2
|
||||
}
|
||||
},
|
||||
|
||||
"world": [
|
||||
@ -437,6 +436,12 @@
|
||||
},
|
||||
{
|
||||
"name": "OPTIONS"
|
||||
},
|
||||
{
|
||||
"name": "ACHIEVEMENTS"
|
||||
},
|
||||
{
|
||||
"name": "STATS"
|
||||
}
|
||||
],
|
||||
|
||||
@ -461,18 +466,24 @@
|
||||
"teleport": "resource/grow_0.wav",
|
||||
"walk": "resource/bump_4.wav",
|
||||
"reverse": "resource/bump_5.wav",
|
||||
"main": "resource/azu main theme_amp.ogg",
|
||||
"menu": "resource/azu menu music_amp.ogg",
|
||||
"take": "resource/Coin_.wav",
|
||||
"checkpoint": "resource/arrive_0.wav",
|
||||
"bong": "resource/bong.ogg"
|
||||
},
|
||||
"volume": {
|
||||
"chapters":
|
||||
[
|
||||
"resource/bgm/Chapter1.ogg",
|
||||
"resource/bgm/Chapter2.ogg",
|
||||
"resource/bgm/Chapter3.ogg",
|
||||
"resource/bgm/Chapter4.ogg"
|
||||
],
|
||||
"volume":
|
||||
{
|
||||
"restart": 0.3,
|
||||
"teleport": 0.7,
|
||||
"walk": 0.55,
|
||||
"reverse": 0.5,
|
||||
"main": 1.0,
|
||||
"menu": 1.0,
|
||||
"take": 0.5,
|
||||
"checkpoint": 0.5,
|
||||
@ -481,7 +492,8 @@
|
||||
"fade": 2.0,
|
||||
"muted": false,
|
||||
"bgm muted": false,
|
||||
"sfx muted": false
|
||||
"sfx muted": false,
|
||||
"test enabled": false
|
||||
},
|
||||
|
||||
"ending":
|
||||
@ -518,6 +530,29 @@
|
||||
"message scale": [1.05, 0.125]
|
||||
},
|
||||
|
||||
"arcade":
|
||||
{
|
||||
"arcade only": false,
|
||||
"idle timeout": 18.0,
|
||||
"countdown display timeout": 9.0,
|
||||
"credits required": 0.0,
|
||||
"credit increase per event": 1.0,
|
||||
"max credits": 0.0,
|
||||
"credits enabled": false,
|
||||
"credit name": "CREDIT",
|
||||
"start message": "PUSH TO PLAY",
|
||||
"add credits message": "ADD {amount} {name}",
|
||||
"credits available":
|
||||
{
|
||||
"text": "{name}: {amount}",
|
||||
"font": "medium",
|
||||
"scale": 0.035,
|
||||
"foreground": [255.0, 255.0, 255.0, 255.0],
|
||||
"background": [60.0, 60.0, 60.0, 200.0],
|
||||
"translation": [0.0, -0.75]
|
||||
}
|
||||
},
|
||||
|
||||
"diagnostic":
|
||||
{
|
||||
"fps scale": 0.018,
|
||||
@ -672,7 +707,7 @@
|
||||
{
|
||||
"id": "ACH_ICING",
|
||||
"name": "Icing",
|
||||
"description": "Walk backward between the two checkpoints on level 2 without going forward"
|
||||
"description": "Walk backward between checkpoints on level 2 without going forward"
|
||||
},
|
||||
{
|
||||
"id": "ACH_BATTER_UP",
|
||||
@ -726,7 +761,7 @@
|
||||
{
|
||||
"id": "ACH_BOOMERINGUE",
|
||||
"name": "Boomeringue",
|
||||
"description": "Carry the coin back to the start of level 7 and finish the level without dying"
|
||||
"description": "Carry the coin to the start of level 7 and finish without dying"
|
||||
},
|
||||
{
|
||||
"id": "ACH_BACK_IN_THE_OVEN",
|
||||
@ -821,7 +856,7 @@
|
||||
{
|
||||
"id": "ACH_C8KE",
|
||||
"name": "C8ke",
|
||||
"description": "Play Cakefoot for eight days in a row",
|
||||
"description": "Play Cakefoot during eight days in a row",
|
||||
"stat":
|
||||
{
|
||||
"id": "STAT_CONSECUTIVE_DAYS_PLAYED",
|
||||
@ -859,8 +894,7 @@
|
||||
[
|
||||
{
|
||||
"id": "STAT_DISTANCE_TRAVELED",
|
||||
"name": "Distance traveled",
|
||||
"type": "FLOAT"
|
||||
"name": "Distance traveled"
|
||||
},
|
||||
{
|
||||
"id": "STAT_SLICER_DEATHS",
|
||||
@ -955,7 +989,7 @@
|
||||
},
|
||||
{
|
||||
"id": "STAT_STRAY_TIMER_MAX",
|
||||
"name": "Stray timer max",
|
||||
"name": "In-place timer max",
|
||||
"increment only": true,
|
||||
"type": "FLOAT"
|
||||
},
|
||||
@ -981,5 +1015,174 @@
|
||||
{
|
||||
"level beaten": "+",
|
||||
"level unbeaten": "-"
|
||||
},
|
||||
|
||||
"achievements menu":
|
||||
{
|
||||
"background":
|
||||
{
|
||||
"scale": [1.7, 0.725],
|
||||
"color": [0.1, 0.1, 0.1, 0.7],
|
||||
"position": [0.0, 0.22]
|
||||
},
|
||||
"text":
|
||||
{
|
||||
"color": [1.0, 1.0, 1.0],
|
||||
"scale": [0.425, 0.0725],
|
||||
"start": [-1.274, 0.875],
|
||||
"step": [0.85, -0.145],
|
||||
"dimensions": [270.0, 55.0],
|
||||
"wrap": 260
|
||||
},
|
||||
"rows": 10,
|
||||
"unlocked": [0.0, 0.45, 0.0, 0.7],
|
||||
"locked": [0.0, 0.0, 0.0, 0.0],
|
||||
"pop up":
|
||||
{
|
||||
"font": "narrow progress menu",
|
||||
"foreground": [1.0, 1.0, 1.0, 1.0],
|
||||
"background": [0.0, 0.45, 0.0, 0.7],
|
||||
"scale": [0.78, 0.13],
|
||||
"position": [0.0, 0.87],
|
||||
"dimensions": [540.0, 90.0],
|
||||
"heading": "NEW ACHIEVEMENT!",
|
||||
"limit": 2
|
||||
}
|
||||
},
|
||||
|
||||
"stats menu":
|
||||
{
|
||||
"font": "medium",
|
||||
"foreground": [1.0, 1.0, 1.0, 1.0],
|
||||
"background": [0.1, 0.1, 0.1, 0.7],
|
||||
"position": [0.0, 0.22],
|
||||
"scale": [1.7, 0.525],
|
||||
"columns": [32, 11, 12],
|
||||
"precision": 2
|
||||
},
|
||||
|
||||
"operator":
|
||||
{
|
||||
"enabled": false,
|
||||
"font": "medium",
|
||||
"glyph font": "glyph",
|
||||
"foreground": [255.0, 255.0, 255.0, 255.0],
|
||||
"background": [60.0, 60.0, 60.0, 200.0],
|
||||
"wrap": 1400,
|
||||
"scale": 0.035,
|
||||
"unsaved color": [180.0, 0.0, 0.0, 255.0],
|
||||
"saved color": [0.0, 180.0, 0.0, 255.0],
|
||||
"unsaved message": "There are unsaved changes.",
|
||||
"saved message": "Saved.",
|
||||
"confirmation message": "There are unsaved changes, press again to confirm exit.",
|
||||
"arrow height": 0.5,
|
||||
"labels":
|
||||
{
|
||||
"intro":
|
||||
{
|
||||
"text": "CAKEFOOT ARCADE OPERATOR MENU\n\nUse the joystick to select a menu item. Press the button to toggle or to start editing. When editing, use joystick up/down to select a character, then press the button to add the character. Scroll to the end for the confirm and backspace characters.\n\nFor support, contact mark@wonderville.nyc",
|
||||
"translation": [0.0, 0.7],
|
||||
"scale": [1.7, 0.21]
|
||||
},
|
||||
"enable credits":
|
||||
{
|
||||
"text": "Enable credits:",
|
||||
"translation": [-1.42, 0.35]
|
||||
},
|
||||
"credits required":
|
||||
{
|
||||
"text": "Credits required per play:",
|
||||
"translation": [-1.22, 0.2]
|
||||
},
|
||||
"credit increase":
|
||||
{
|
||||
"text": "Credit increase per event:",
|
||||
"translation": [-1.22, 0.05]
|
||||
},
|
||||
"credit display":
|
||||
{
|
||||
"text": "Credit display name:",
|
||||
"translation": [-1.33, -0.1]
|
||||
},
|
||||
"status":
|
||||
{
|
||||
"text": "{status}",
|
||||
"translation": [-0.5, -0.85]
|
||||
},
|
||||
"wi-fi network":
|
||||
{
|
||||
"text": "Wi-fi network:",
|
||||
"translation": [-1.435, -0.25]
|
||||
},
|
||||
"wi-fi password":
|
||||
{
|
||||
"text": "Wi-fi password:",
|
||||
"translation": [-1.4175, -0.4]
|
||||
}
|
||||
},
|
||||
"input":
|
||||
{
|
||||
"credits required":
|
||||
{
|
||||
"translation": [-0.45, 0.2],
|
||||
"selection": "decimal",
|
||||
"max": 6
|
||||
},
|
||||
"credit increase":
|
||||
{
|
||||
"translation": [-0.45, 0.05],
|
||||
"selection": "decimal",
|
||||
"max": 6
|
||||
},
|
||||
"credit display":
|
||||
{
|
||||
"translation": [-0.45, -0.1],
|
||||
"selection": "uppercase",
|
||||
"max": 8
|
||||
},
|
||||
"wi-fi network":
|
||||
{
|
||||
"translation": [-0.45, -0.25],
|
||||
"selection": "all",
|
||||
"max": 32,
|
||||
"allow empty": true
|
||||
},
|
||||
"wi-fi password":
|
||||
{
|
||||
"translation": [-0.45, -0.4],
|
||||
"selection": "all",
|
||||
"max": 63,
|
||||
"allow empty": true
|
||||
}
|
||||
},
|
||||
"buttons":
|
||||
{
|
||||
"save":
|
||||
{
|
||||
"text": "APPLY & SAVE",
|
||||
"translation": [-1.2, -0.6],
|
||||
"dimensions": [350, 40],
|
||||
"scale": 0.4
|
||||
},
|
||||
"exit":
|
||||
{
|
||||
"text": "EXIT",
|
||||
"translation": [-1.2, -0.7],
|
||||
"dimensions": [350, 40],
|
||||
"scale": 0.4
|
||||
},
|
||||
"enable credits":
|
||||
{
|
||||
"checked texture": "check",
|
||||
"unchecked texture": "x",
|
||||
"translation": [-0.45, 0.35],
|
||||
"scale": 0.045,
|
||||
"aspect ratio": 1.0
|
||||
}
|
||||
},
|
||||
"order": [
|
||||
"enable credits", "credits required", "credit increase", "credit display", "wi-fi network",
|
||||
"wi-fi password", "save", "exit"
|
||||
]
|
||||
}
|
||||
}
|
233
doc/Cakefoot_Arcade_Operators_Manual.md
Normal file
@ -0,0 +1,233 @@
|
||||
Cakefoot Arcade Operator's Manual
|
||||
=================================
|
||||
|
||||
If Cakefoot is not installed yet, see [Installing Cakefoot as an Arcade Cabinet][0].
|
||||
|
||||
Once the game is installed and running in arcade-only mode, an operator's menu will be available. This menu can be used to set the amount of credits required to play, and to connect to a WiFi network for automatic updates.
|
||||
|
||||
Advanced operations can be performed by connecting a USB keyboard, exiting the game, and [opening the system terminal][6].
|
||||
|
||||
[0]: Installing_Cakefoot_as_an_Arcade_Cabinet.md
|
||||
[6]: #advanced-system-configuration
|
||||
|
||||
Operator's menu
|
||||
---------------
|
||||
|
||||
Press the operator's button on the inside of the Cakefoot arcade cabinet while Cakefoot is at the title screen. A menu with options specific to the arcade version should appear. In case the operator's button is not working or not installed, attach a USB keyboard to the computer and press the `F7` key.
|
||||
|
||||
<img src="Cakefoot_operator_menu.png" width=700 />
|
||||
|
||||
### Controls ###
|
||||
|
||||
Use up and down on the joystick to move between menu options. Press any button to start editing an option's value.
|
||||
|
||||
If the value is text or a number, the character input widget will appear. Use up and down to go through the list of characters. Select a character and press any button to add it to the option value.
|
||||
|
||||
The special characters backspace (⇦) and confirm (⇰) can also be input. Scroll to the end of the list to find those characters. Input a backspace character to delete a character. Input a confirm character to finish editing.
|
||||
|
||||
### Credits ###
|
||||
|
||||
The `Credits required per play` option is used to set the number of credits required to start the game. For example, if `4` credits must be inserted before the game should start, set the value to `4.0`.
|
||||
|
||||
The `Credit increase per event` option is used to set how much the credit amount should increase per trigger of the credit key. For example, if using tokens, and every token should be worth `2` credits instead of `1`, set the value to `2.0`.
|
||||
|
||||
The `Credit display name` option sets the name shown on the title screen to display how many credits the player has. For example, if using tokens instead of credits, the name could be set to `TOKEN`.
|
||||
|
||||
### WiFi ###
|
||||
|
||||
The `Wi-fi network` option is used to enter the name of a WiFi network to connect to. Uppercase, lowercase, space, and special characters are supported.
|
||||
|
||||
The `Wi-fi password` option is used to enter the password for the WiFi network. Uppercase, lowercase, space, and special characters are supported.
|
||||
|
||||
After changing the WiFi parameters, press `APPLY & SAVE` to apply the WiFi settings. There is currently no indicator of whether the WiFi is successfully connected, so make sure the parameters are input correctly. A future update is planned to display an icon when network connection is successful.
|
||||
|
||||
### Apply and save ###
|
||||
|
||||
Settings are not applied until the `APPLY & SAVE` button is pressed. To reset the changes, press the `EXIT` button twice to exit the menu without applying.
|
||||
|
||||
Advanced system configuration
|
||||
-----------------------------
|
||||
|
||||
Some advanced operations are only possible to do from the system terminal. For example, clearing high scores and changing the screen resolution. These operations may be added to the game's interface in the future, but for now it is necessary to close Cakefoot and log into the system terminal.
|
||||
|
||||
The standard Cakefoot installation uses headless Linux, meaning there is no windowing system, only the system terminal. To quit to the terminal while Cakefoot is running, attach a USB keyboard to the computer and press `SHIFT+CTRL+Q`. The shift key prevents Cakefoot from trying to automatically relaunch after being closed, which is the default behavior. Once the game is closed, a login prompt will appear. If the password has not been changed, enter user `cakefoot` and password `forkitup`. See below for how to change the password after logging in. If it is not possible to get to the terminal using the key combination, see [Crash loop][4].
|
||||
|
||||
All edits must be made using the keyboard and terminal. For the operations in this section, the instructions contain exactly what to input into the terminal. However, it may still be useful to read [The Linux command line for beginners][1] if necessary.
|
||||
|
||||
[1]: https://ubuntu.com/tutorials/command-line-for-beginners
|
||||
[4]: #crash-loop
|
||||
|
||||
### Reverting an update ###
|
||||
|
||||
If the system isn't working, it's possible a bad or broken update was installed. In that case, it may be possible to restore to a previously installed version.
|
||||
|
||||
Delete the WiFi connection so no further updates will be downloaded. Later, when a new, working update is available, WiFi can be restored. Enter the following, replacing the name with the actual name of the WiFi network currently in use.
|
||||
|
||||
> sudo nmcli connection delete "Spicy Meatball"
|
||||
|
||||
Once logged into the system, change to the Cakefoot install directory.
|
||||
|
||||
> cd ~/cakefoot
|
||||
|
||||
List the directory with details to see how the installation is currently set up to run.
|
||||
|
||||
> ls -l
|
||||
total 23424
|
||||
lrwxrwxrwx 1 cakefoot cakefoot 33 Feb 27 12:34 Cakefoot-linux -> Cakefoot-linux_arcade-1.5.0
|
||||
drwxrwxr-x 7 cakefoot cakefoot 4096 Feb 27 12:34 Cakefoot-linux_arcade-1.4.2
|
||||
drwxrwxr-x 7 cakefoot cakefoot 4096 Feb 27 01:58 Cakefoot-linux_arcade-1.4.3
|
||||
drwxrwxr-x 7 cakefoot cakefoot 4096 Feb 27 02:53 Cakefoot-linux_arcade-1.5.0
|
||||
|
||||
`Cakefoot-linux` is a link to the folder corresponding to the version that will be launched at start up. In this case, it is linked to `Cakefoot-linux_arcade-1.5.0`. Assume 1.5.0 is a bad update, and use `ln` to link instead to 1.4.3, the previous version.
|
||||
|
||||
> ln -nsf Cakefoot-linux_arcade-1.4.3 Cakefoot-linux
|
||||
> ls -l
|
||||
total 23424
|
||||
lrwxrwxrwx 1 cakefoot cakefoot 33 Feb 27 12:34 Cakefoot-linux -> Cakefoot-linux_arcade-1.4.3
|
||||
drwxrwxr-x 7 cakefoot cakefoot 4096 Feb 27 12:34 Cakefoot-linux_arcade-1.4.2
|
||||
drwxrwxr-x 7 cakefoot cakefoot 4096 Feb 27 01:58 Cakefoot-linux_arcade-1.4.3
|
||||
drwxrwxr-x 7 cakefoot cakefoot 4096 Feb 27 02:53 Cakefoot-linux_arcade-1.5.0
|
||||
|
||||
`Cakefoot-linux` is now linked to `Cakefoot-linux_arcade-1.4.3`. This means the 1.4.3 version is active. Restart Cakefoot, and the system should be working again.
|
||||
|
||||
> systemctl --user restart cakefoot
|
||||
|
||||
When a new update is available, or it is confirmed that the issue was not the update, automatic updates can be resumed by reapplying the WiFi settings in the operator's menu.
|
||||
|
||||
### Switching between HDMI and AUX audio ###
|
||||
|
||||
The file `~/.asoundrc` is used to configure the sound card and sound device. The device index in this file can be either `0` to indicate AUX or `3` to indicate HDMI. Use the command below to set the sound device to HDMI. To use AUX instead, use `0` in place of `3`.
|
||||
|
||||
> echo -e 'defaults.pcm.!card 0\ndefaults.ctl.!card 0\ndefaults.pcm.!device 3\ndefaults.ctl.!device 3' > ~/.asoundrc
|
||||
|
||||
### Changing the resolution ###
|
||||
|
||||
The resolution can be set in the configuration file, using the parameter at `display > dimensions`. To override this parameter, add a custom configuration file to the `config/` folder. Configuration files are read in alphabetical order, so make sure the file name does not come before the config files already in the folder.
|
||||
|
||||
> ls -v ~/cakefoot/Cakefoot-linux/config
|
||||
config.json config_arcade.json config_arcade_cabinet.json
|
||||
> echo '{"display": {"dimensions": [1280, 720]}}' > ~/cakefoot/Cakefoot-linux/config/resolution.json
|
||||
> ls -v ~/cakefoot/Cakefoot-linux/config
|
||||
config.json config_arcade.json config_arcade_cabinet.json resolution.json
|
||||
|
||||
### Clearing scores ###
|
||||
|
||||
Scores are stored in a JSON file in the `storage/` folder. Move that file out of the directory into the home folder to save it as a backup. The game will automatically create a new arcade scores file the next time a score is submitted.
|
||||
|
||||
> mv ~/cakefoot/Cakefoot-linux/storage/cakefoot_arcade_scores.json ~/scores_backup.json
|
||||
|
||||
### Change password ###
|
||||
|
||||
To change the password for the `cakefoot` user, use the Linux `passwd` command. Enter `passwd` into the terminal and follow the prompts.
|
||||
|
||||
> passwd
|
||||
|
||||
### Other configuration ###
|
||||
|
||||
For advanced users, there are more parameters that can be edited in the configuration file. Use `less` to read the configuration or browse the file at [config.json][8]. Then use the same technique as in [Changing the resolution][7] to override a particular value.
|
||||
|
||||
> less ~/cakefoot/Cakefoot-linux/config/config.json
|
||||
|
||||
[7]: #changing-the-resolution
|
||||
[8]: ../config/config.json
|
||||
|
||||
Help
|
||||
----
|
||||
|
||||
### Crash loop ###
|
||||
|
||||
If the game crashes, only to be reloaded and then crash again repeatedly, the automatic reload service needs to be turned off. If Cakefoot loads to the title screen before it crashes, it's possible to quit the game and stop the service at the same time by pressing `SHIFT+CTRL+Q` on the title screen. When the game is quit with that key combination, it should not reload automatically.
|
||||
|
||||
If the game does not reach the title screen before crashing, it won't be possible to use the key combination to quit. In that case, the only option is to input commands directly into the terminal during the time between relaunches. Between relaunches, the login prompt should appear on screen before the next launch happens. While the login prompt is on screen, enter the username and password (the defaults are `cakefoot` and `forkitup`).
|
||||
|
||||
Once logged in, enter the following command into the terminal to stop the reload service.
|
||||
|
||||
> systemctl --user stop cakefoot
|
||||
|
||||
After that, the game should not relaunch automatically anymore. Most likely, the next step would be [Reverting an update][2].
|
||||
|
||||
[2]: #reverting-an-update
|
||||
|
||||
### Cakefoot does not load ###
|
||||
|
||||
Most likely, the best thing to do would be to follow the instructions in [Reverting an update][2]. There is also the option of looking at the logs to see if there is any helpful error message.
|
||||
|
||||
> journalctl --user -u cakefoot
|
||||
|
||||
### Updates do not download or install ###
|
||||
|
||||
Since there is no feature showing WiFi connectivity built into the game yet, the terminal must be used to test if there is a network connection available. `NetworkManager` is installed for managing WiFi connections. Try checking the network status to see if internet is working.
|
||||
|
||||
> nmcli general status
|
||||
STATE CONNECTIVITY WIFI-HW WIFI WWAN-HW WWAN METERED
|
||||
connected full enabled enabled missing enabled no (guessed)
|
||||
|
||||
The state should be `connected`, and connectivity should be `full`. If not, try looking up the available connections to see if the WiFi network has been added successfully.
|
||||
|
||||
> nmcli connection show --active
|
||||
NAME UUID TYPE DEVICE
|
||||
Spicy Meatball 745ae677-0b49-4bba-9111-eab25d7f94f7 wifi wlp0s20f3
|
||||
|
||||
This indicates "Spicy Meatball" is successfully connected. If the network isn't listed, try listing all connections.
|
||||
|
||||
> nmcli connection show
|
||||
NAME UUID TYPE DEVICE
|
||||
lo 9f35a928-850c-4c63-b82e-012ff7823f55 loopback lo
|
||||
Spicy Meatball 745ae677-0b49-4bba-9111-eab25d7f94f7 wifi --
|
||||
|
||||
This indicates "Spicy Meatball" has been added to NetworkManager but is not connected. In that case, try deleting the connection before reconnecting it. If the network isn't listed, skip this step.
|
||||
|
||||
> nmcli connection delete "Spicy Meatball"
|
||||
|
||||
Add the network.
|
||||
|
||||
> nmcli device wifi connect "Spicy Meatball" password "B00M->H0t$4uc3"
|
||||
Device 'wlp0s20f3' successfully activated with '3172be6c-6469-435b-8d23-05d9e8552b6f'.
|
||||
|
||||
The output should indicate the WiFi device was successfully activated. Restart Cakefoot and automatic updates should resume.
|
||||
|
||||
> systemctl --user start cakefoot
|
||||
|
||||
For further information, see [RedHat's NetworkManager manual][3].
|
||||
|
||||
[3]: https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/7/html/networking_guide/sec-Configuring_IP_Networking_with_nmcli
|
||||
|
||||
### There is no sound ###
|
||||
|
||||
See [Switching between HDMI and AUX audio][9] for instructions on how to switch to a different sound device. If sound is still not working, there is a terminal GUI available for making sound adjustments. Try running `alsamixer` to see if there is a sound setting that needs to be adjusted. If the adjustments are successful, save the settings afterward.
|
||||
|
||||
> alsamixer
|
||||
> sudo alsactl store
|
||||
|
||||
[9]: #switching-between-HDMI-and-AUX-audio
|
||||
|
||||
### High scores disappeared ###
|
||||
|
||||
This could be the result of file corruption during a save operation or an error during a software update. If there is an existing file at `~/cakefoot/Cakefoot-linux/storage/cakefoot_arcade_scores.json` that isn't being read into the game, it may contain corrupted data. First, backup the file in the home directory in case it can be restored later.
|
||||
|
||||
> ls ~/cakefoot/Cakefoot-linux/storage/
|
||||
cakefoot_arcade_scores.json cakefoot_preferences.json cakefoot_progress.json cakefoot_stats.json
|
||||
> mv ~/cakefoot/Cakefoot-linux/storage/cakefoot_arcade_scores.json ~/corrupt_scores_backup.json
|
||||
|
||||
Then, to see if there is a file that can be restored from a previous update, check for previous versions in the `~/cakefoot/` folder.
|
||||
|
||||
> ls ~/cakefoot/
|
||||
Cakefoot-linux Cakefoot-linux_arcade-1.4.2 Cakefoot-linux_arcade-1.4.3
|
||||
> ls ~/cakefoot/Cakefoot-linux_arcade-1.4.2/storage/
|
||||
cakefoot_arcade_scores.json cakefoot_preferences.json cakefoot_progress.json cakefoot_stats.json
|
||||
|
||||
In this example, the 1.4.2 version has a scores file. Try copying that into the current version folder to see if any scores are displayed.
|
||||
|
||||
> cp ~/cakefoot/Cakefoot-linux_arcade-1.4.2/storage/cakefoot_arcade_scores.json ~/cakefoot/Cakefoot-linux/storage/
|
||||
|
||||
Try running the game to see if any scores were restored. If not, keep trying older versions if there are any left.
|
||||
|
||||
### Debugging ###
|
||||
|
||||
For advanced users, debug files can be generated with potentially helpful output for fixing errors. Use the technique described in [Changing the resolution][7] to enable debug files in the configuration. The values to set to `true` are `log > file enabled` and `log > debug to file`. After the game is run, log files should appear in the `log/` folder.
|
||||
|
||||
> echo '{"log": {"file enabled": true, "debug to file": true}}' > ~/cakefoot/Cakefoot-linux/config/log.json
|
||||
> systemctl --user start cakefoot
|
||||
> ls ~/cakefoot/Cakefoot-linux/log/
|
||||
cakefoot_debug.log cakefoot_info.log
|
||||
> less ~/cakefoot/Cakefoot-linux/log/cakefoot_debug.log
|
BIN
doc/Cakefoot_cabinet_preview_800x900.png
Normal file
After ![]() (image error) Size: 550 KiB |
BIN
doc/Cakefoot_cabinet_vinyl_install.jpg
Normal file
After ![]() (image error) Size: 6.1 MiB |
BIN
doc/Cakefoot_header.png
Normal file
After ![]() (image error) Size: 99 KiB |
BIN
doc/Cakefoot_operator_menu.png
Normal file
After ![]() (image error) Size: 180 KiB |
BIN
doc/Cakefoot_thumbnail.png
Normal file
After ![]() (image error) Size: 26 KiB |
230
doc/Installing_Cakefoot_as_an_Arcade_Cabinet.md
Normal file
@ -0,0 +1,230 @@
|
||||
# Installing Cakefoot as an Arcade Cabinet
|
||||
|
||||
Automatically load and run Cakefoot at startup in arcade-only mode by following the steps below!
|
||||
|
||||
<img src="Cakefoot_cabinet_preview_800x900.png" width=400 />
|
||||
|
||||
## PC
|
||||
|
||||
These instructions will install Linux and Cakefoot onto a PC, so that the PC becomes a dedicated Cakefoot arcade machine. It should be possible to adapt the instructions to work for other games as well, as long as they have a Linux build. The specific PC used is a Beelink MINI S. In general, it's important to check whether a PC's hardware is compatible with Linux. The Beelink, for example, required the alpha version of Debian 13 because the PC's WiFi card only recently received support.
|
||||
|
||||
A working Linux build of Cakefoot is a prerequisite if following the steps below. It is possible to build the game directly on the PC, but that is not covered by the instructions. See the [Build][8] section for how to get started building Cakefoot.
|
||||
|
||||
Other operating systems, like Windows and macOS can also run Cakefoot in arcade-only mode with all features other than automatic updates, but the instructions do not cover those operating systems. See [Andy Wallace's post][6] for a Windows installation method that works with the Windows Cakefoot build.
|
||||
|
||||
Familiarity with the terminal is also a prerequisite. See [The Linux command line for beginners][12] to get started using the terminal.
|
||||
|
||||
[6]: https://www.tumblr.com/andymakesgames/775682484969029632/setting-up-a-windows-11-computer-as-a-permanent
|
||||
[8]: ../README.md#build
|
||||
[12]: https://ubuntu.com/tutorials/command-line-for-beginners
|
||||
|
||||
## Create a bootable SD card for installing Linux
|
||||
|
||||
Using another computer, create a bootable OS installer that will replace the Beelink MINI S PC's built-in Windows OS with Debian Linux 13. These instructions will use an SD card, but a USB stick can also be used. Make sure it is empty or unused because the contents will be erased.
|
||||
|
||||
1. [Download balenaEtcher][4] for creating a bootable SD card. For example, this downloads version 1.19.25.
|
||||
|
||||
wget https://github.com/balena-io/etcher/releases/download/v1.19.25/balenaEtcher-linux-x64-1.19.25.zip
|
||||
unzip balenaEtcher-linux-x64-1.19.25.zip
|
||||
|
||||
2. Download the [Debian testing netinst ISO][5]. The following will download Debian 13 alpha.
|
||||
|
||||
wget https://cdimage.debian.org/cdimage/trixie_di_alpha1/amd64/iso-cd/debian-trixie-DI-alpha1-amd64-netinst.iso
|
||||
|
||||
3. Launch Balena etcher. It may be necessary to run as root, see the alternative command below if there are errors.
|
||||
|
||||
balenaEtcher-linux-x64/balena-etcher
|
||||
# alternately: sudo balenaEtcher-linux-x64/balena-etcher --no-sandbox
|
||||
|
||||
4. Load the Debian ISO and connect and select the SD card. Then press the "Flash" button.
|
||||
|
||||
5. Once the SD card is flashed, remove it and connect it to the Beelink MINI S PC.
|
||||
|
||||
[4]: https://github.com/balena-io/etcher/releases
|
||||
[5]: https://www.debian.org/devel/debian-installer/
|
||||
|
||||
## Install Debian
|
||||
|
||||
Replace Windows with Debian Linux using the installer. For further details on any of these steps or if any issues arise, see the [Debian installation guide][7].
|
||||
|
||||
6. Turn on the Beelink MINI S PC and press F7 to bring up the boot menu. Choose the option showing the SD card. For example, it might be something like `UEFI: Generic MassStorageClass1536`.
|
||||
|
||||
7. The installer should load. Choose `Graphical install`. Then choose the appropriate language and keyboard settings.
|
||||
|
||||
8. The installer should detect both ethernet and WiFi interfaces. Choose ethernet if using a wired connection. Otherwise, choose WiFi and set up the network.
|
||||
|
||||
9. Choose the system name. It can be any single word, like "cakefoot". Leave the domain name blank.
|
||||
|
||||
10. In the next screen, skip setting up the root user and just choose `Continue`. That will disable the root account and enable the first user account to be the administrator.
|
||||
|
||||
11. Skip the full name entry by choosing `Continue` since it won't be relevant on this system.
|
||||
|
||||
12. Enter a username for the user account, like "cakefoot". It's OK if it's the same as the system name. Then choose a password.
|
||||
|
||||
13. Choose the appropriate time zone for the system.
|
||||
|
||||
14. In the disk partitioning step, the entire hard drive will be erased and reformatted. Windows will be erased, but if it was previously activated, it can later be restored if necessary using an ISO downloaded from Microsoft. Choose `Guided - use entire disk`. Make sure to choose the PC's hard drive and not the SD card. For example, the hard drive may be something like `SCSI1 (0,0,0)(SDA) - 512.1 GB ATA FORESEE 512GB SS`. Then, choose `All files in one partition` and `Finish partitioning and write changes to disk`.
|
||||
|
||||
15. Continue to install the minimal system.
|
||||
|
||||
16. At the software selection screen, uncheck `Debian desktop environment` and `GNOME`. Check off only `standard system utilities` and `SSH server`. A desktop environment is not necessary for running Cakefoot.
|
||||
|
||||
17. Once the software is installed, the installation should complete. Remove the SD card and choose `Continue` to reboot.
|
||||
|
||||
[7]: https://www.debian.org/releases/stable/installmanual
|
||||
|
||||
## Transfer the build
|
||||
|
||||
This transfer method requires the build to be on a USB stick or SD card. Other transfer options include downloading the build and compiling from source on the PC.
|
||||
|
||||
18. When the PC reboots, let the menu choose Debian automatically, and the login prompt will appear. Enter the username and password configured during installation.
|
||||
|
||||
19. Increase the font size on the console if necessary. Choose `Terminus` and `16x32` to make the font large.
|
||||
|
||||
sudo dpkg-configure console-setup
|
||||
|
||||
20. Plug in the USB drive with the build, and look at the output of `fdisk -l` to find the drive's device name. In this case, it's `/dev/sdb` because the USB drive capacity is 16GB.
|
||||
|
||||
> sudo apt install unzip
|
||||
> fdisk -l
|
||||
...
|
||||
Disk /dev/sdb: 14.46 GiB
|
||||
...
|
||||
|
||||
21. Make a directory where the USB drive will be mounted, and mount the drive with permissions for all users to access the files.
|
||||
|
||||
sudo mkdir /media/usb_drive
|
||||
sudo mount -o "uid=1000,gid=1000" /dev/sdb /media/usb_drive/
|
||||
|
||||
22. Make a directory for the game. Transfer the build into the new folder and unzip it if it's not unzipped already. Finish by un-mounting the USB drive, so it can be unplugged safely.
|
||||
|
||||
mkdir ~/cakefoot/
|
||||
cp -r /media/usb_drive/Cakefoot/Cakefoot-linux-1.4.2.zip ~/cakefoot/
|
||||
cd ~/cakefoot
|
||||
unzip Cakefoot-linux-1.4.2.zip
|
||||
sudo umount /media/usb_drive
|
||||
|
||||
## Configure the OS and the game and run a test
|
||||
|
||||
Before running the game, some dependencies need to be installed, and some audio and video settings need to be edited.
|
||||
|
||||
23. Install Open GL and SDL for video, and ALSA utils for audio.
|
||||
|
||||
sudo apt update
|
||||
sudo apt install alsa-utils libsdl2-2.0-0 libsdl2-image-2.0-0 libsdl2-ttf-2.0-0 libsdl2-mixer-2.0-0 mesa-utils
|
||||
|
||||
24. Remove SDL shared libraries from the build, so the libraries just installed are used instead. This seems to be necessary for using the KMS DRM video option with SDL, which is required when running the game without a windowing system.
|
||||
|
||||
cd ~/cakefoot
|
||||
rm lib/libSDL2*
|
||||
|
||||
25. Set the master volume and save the setting.
|
||||
|
||||
amixer set Master 100%
|
||||
sudo alsactl store
|
||||
|
||||
26. For the Beelink MINI S, AUX sound should now work. For other systems or for using HDMI instead of AUX, more configuration is required. Use the GUI `alsamixer` and the output of `amixer` to see which controls connect to HDMI. In this case, the controls for `IEC958` are the HDMI settings. Make sure they are turned on.
|
||||
|
||||
> amixer # or alsamixer
|
||||
...Simple mixer control 'IEC958',0...
|
||||
...Simple mixer control 'IEC958',1...
|
||||
...Simple mixer control 'IEC958',2...
|
||||
...Simple mixer control 'IEC958',3...
|
||||
> amixer set IEC958,0 on
|
||||
> amixer set IEC958,1 on
|
||||
> amixer set IEC958,2 on
|
||||
> amixer set IEC958,3 on
|
||||
> sudo alsactl store
|
||||
|
||||
27. To set the sound to output to a specific device, for example HDMI, get the device number from `aplay` and create a configuration at `~/.asoundrc`. This can be switched back later by replacing "device 3" with "device 0".
|
||||
|
||||
> aplay -l
|
||||
...
|
||||
card 0: PCH [HDA Intel PCH], device 3: HDMI 0 [PROJECTOR]
|
||||
...
|
||||
> echo -e 'defaults.pcm.!card 0\ndefaults.ctl.!card 0\ndefault.pcm.!device 3\ndefaults.ctl.!device 3' > ~/.asoundrc
|
||||
|
||||
28. Add the user to the input group, so the game can receive input, and apply the setting.
|
||||
|
||||
sudo usermod -a -G input $USER
|
||||
exec sudo su -l $USER
|
||||
|
||||
29. Cakefoot needs to be configured to run in arcade-only mode. In Cakefoot's git repository, there are configuration files at [`src_config_arcade.json`][13] and [`src/config_arcade_cabinet.json`][14] that can be placed in the `config/` folder to configure arcade-only mode automatically. The second file configures the game to require a credit to be inserted, but that setting can be changed later, using an in-game menu. Some settings in these files can be modified if necessary. For example, to use a different screen resolution, the `display > dimensions` setting can be edited. If using the same USB drive method as above, transfer the configuration files onto the USB, and copy them into the `config/` folder.
|
||||
|
||||
cp /media/usb_drive/config_arcade.json /media/usb_drive/config_arcade_cabinet.json \
|
||||
~/cakefoot/Cakefoot-linux/config/
|
||||
|
||||
30. Cakefoot should now run successfully with sound and input in arcade-only mode. Launch the game and use the keyboard keys to test the game.
|
||||
|
||||
cd ~/cakefoot/Cakefoot-linux
|
||||
./Cakefoot-linux.x64
|
||||
|
||||
[13]: ../src/config_arcade.json
|
||||
[14]: ../src/config_arcade_cabinet.json
|
||||
|
||||
## Launch game on start up
|
||||
|
||||
The `systemd` tool can be used to configure the system to launch the game automatically at startup. The file [`src/cakefoot.service`][15] is used to create a `systemd` service to run Cakefoot. This file needs to be installed on the system.
|
||||
|
||||
31. Use the USB drive to copy the file into the user's `systemd` configurations folder at `~/.config/systemd/user/`, and run `enable` to add the service to the system.
|
||||
|
||||
mkdir -p ~/.config/systemd/user
|
||||
cp /media/usb_drive/cakefoot.service ~/.config/systemd/user
|
||||
systemctl --user enable cakefoot
|
||||
|
||||
32. To allow the service to run without the user logging in, the user must be added to `linger`.
|
||||
|
||||
sudo loginctl enable-linger $USER
|
||||
|
||||
33. The service should run successfully. Because this service re-launches the game automatically when it quits, the command `CTRL+SHIFT+q` must be used to stop the service and fully quit the game.
|
||||
|
||||
systemctl --user start cakefoot
|
||||
|
||||
34. Quit the game, reboot the system, and the game should load and run automatically.
|
||||
|
||||
sudo reboot
|
||||
|
||||
[15]: ../src/cakefoot.service
|
||||
|
||||
## Operator's menu
|
||||
|
||||
The configuration parameters in `config/cakefoot_arcade_cabinet.json` enable access to the arcade operator's menu which contains settings specific to the arcade cabinet version of Cakefoot, including the ability to require coins and the ability to connect to a WiFi network. There is a separate document for the [Operator's menu][9].
|
||||
|
||||
[9]: Cakefoot_Arcade_Operators_Manual.md
|
||||
|
||||
## Advanced
|
||||
|
||||
Once Cakefoot is running in arcade mode, there are a couple of advanced features that can be set up.
|
||||
|
||||
### Auto Power-on
|
||||
|
||||
To turn all the arcade hardware on simultaneously, for example, monitor and speakers, in addition to the PC, it can be useful to plug everything into a power strip which can power everything on or off at the same time. Then, the PC can be configured to run automatically when it receives power, so the whole system can be powered on with one switch.
|
||||
|
||||
Cakefoot and the operating system are still being tested in terms of how they deal with the power cutting out. If an operation is in progress when the power is turned off, data corruption is possible. Future updates should add a feature to the operator's menu allowing the PC to safely shutdown before the power is turned off.
|
||||
|
||||
* Turn on the PC and press `F7` to bring up the boot menu. Choose `"Enter Setup"` in the menu that appears to enter the BIOS menu.
|
||||
|
||||
* Go to `"Chipset" > "PCH-IO Configuration" > "State After G3"` and choose `"S0 State"` (see [screenshots][2] of the process)
|
||||
|
||||
* Press F4 to save and exit BIOS.
|
||||
|
||||
[2]: https://buildin.ai/lizong/share/59bf79eb-6be3-4875-88f8-ae1c63e78467
|
||||
|
||||
### Automatic updates
|
||||
|
||||
Automatic updates require access to an online repository of Cakefoot builds. There is an official repository, but access is protected. It is possible to set up a similar repository on another server by reverse engineering [the update script][10].
|
||||
|
||||
Future support may be added for using a USB drive as a repository, so that updates can be installed automatically from a USB drive.
|
||||
|
||||
* Set the URL of the repository in `system > update > location` in `config/cakefoot_arcade_cabinet.json`.
|
||||
|
||||
* If necessary, add authorization credentials to `.netrc`.
|
||||
|
||||
echo "machine spicy.meatball login waluigi password 1tsw4lu1g1t1m3" >> ~/.netrc
|
||||
|
||||
* Add a [custom rule][11] to Linux's polkit system that allows the user to modify network settings without being logged in.
|
||||
|
||||
sudo cp /media/usb_drive/polkit_network_manager_rule.js /etc/polkit-1/rules.d/local-NetworkManager.rules
|
||||
|
||||
[10]: ../src/update_arcade_cabinet.py
|
||||
[11]: ../src/polkit_network_manager_rule.js
|
7
doc/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
| Document | Description |
|
||||
| -------- | ----------- |
|
||||
| [Installing Cakefoot as an Arcade Cabinet][0] | Detailed instruction on how to turn a PC into a Cakefoot arcade machine |
|
||||
| [Arcade Operator's Manual][1] | Software features of the Cakefoot arcade version and fixes for issues |
|
||||
|
||||
[0]: Installing_Cakefoot_as_an_Arcade_Cabinet.md
|
||||
[1]: Cakefoot_Arcade_Operators_Manual.md
|
40
index.html
@ -121,7 +121,7 @@
|
||||
width: 32px;
|
||||
margin: auto;
|
||||
border-radius: 4px;
|
||||
filter: saturate(20%) brightness(130%);
|
||||
filter: saturate(70%) brightness(90%);
|
||||
}
|
||||
|
||||
div#message div#icons img:hover
|
||||
@ -160,7 +160,7 @@
|
||||
|
||||
div#message div#icons
|
||||
{
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,7 +178,7 @@
|
||||
|
||||
div#message div#icons
|
||||
{
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -197,7 +197,7 @@
|
||||
<div id="message">
|
||||
<a id="wishlist" href="https://store.steampowered.com/app/2869020/Cakefoot/" target="_blank">
|
||||
✨️
|
||||
<u>BUY<span class="desktop"> NOW</span> on Steam</u>
|
||||
<u>BUY <span class="desktop">NOW </span>on Steam</u>
|
||||
✨️
|
||||
</a>
|
||||
<div id="ticker">
|
||||
@ -218,22 +218,10 @@
|
||||
<a href="https://ohsqueezy.itch.io/cakefoot" target="_blank">
|
||||
<img src="ext/icon/itch_square_89px.png" />
|
||||
</a>
|
||||
<a href="https://youtube.com/@nuggetsselect" target="_blank">
|
||||
<img src="ext/icon/YouTube_square_89px.png" />
|
||||
</a>
|
||||
<a href="https://www.tiktok.com/@dankd0tgame" target="_blank">
|
||||
<a href="https://www.tiktok.com/@dankdotgame" target="_blank">
|
||||
<img src="ext/icon/TikTok_square_89px.png" />
|
||||
</a>
|
||||
<a href="https://dank.game#email-box" target="_blank" class="desktop">
|
||||
<img src="ext/icon/Email_square_89px.png" />
|
||||
</a>
|
||||
<a href="https://twitch.tv/dankd0tgame" target="_blank">
|
||||
<img src="ext/icon/Twitch_square_89px.png" />
|
||||
</a>
|
||||
<a href="https://open.shampoo.ooo/shampoo/cakefoot" target="_blank" class="desktop">
|
||||
<img src="ext/icon/GitHub_square_89px.png" />
|
||||
</a>
|
||||
<a href="https://dank.game/feed" target="_blank" class="desktop">
|
||||
<a href="https://dank.game/feed" target="_blank">
|
||||
<img src="ext/icon/RSS_square_89px.png" />
|
||||
</a>
|
||||
</div>
|
||||
@ -249,16 +237,28 @@
|
||||
var ticker_ticking = true;
|
||||
var ticker_content = [
|
||||
"Use ☝️, 🖱️, ⌨️ or 🎮 to play",
|
||||
|
||||
"👉️ <a href='https://dank.game#email-box' target='_blank'>Sign up</a> for the newsletter!",
|
||||
|
||||
"Games Ardor includes Cakefoot in " +
|
||||
"<a href='https://gamesardor.net/top-steam-games-to-lookout-for-in-february-2025/'>" +
|
||||
"top Steam games to lookout for in February</a> 🌟",
|
||||
|
||||
"️ Cakefoot is also on 🏪️ " +
|
||||
"<a href='https://store.steampowered.com/app/2869020/Cakefoot/' target='_blank'>Steam</a>, " +
|
||||
"<a href='https://ohsqueezy.itch.io/cakefoot' target='new'>itch.io</a> & " +
|
||||
"<a href='https://coolmathgames.com/0-cakefoot' target='_blank'>Coolmath</a>",
|
||||
|
||||
"Unlock BEEF CAKE 💪️ and BUFFALO BEEF CAKE 🔥️",
|
||||
|
||||
"Collect every coin 💯️ for a different ending",
|
||||
|
||||
"Use code \"THECAKEISNOLIE\" to keep playing ad-free when ads 😱 launch",
|
||||
"Check out the ▶️ <a href='https://youtu.be/xn-iNcUlIpo' target='_blank'>trailer</a> ▶️",
|
||||
"Visit the <a href='https://x.com/wondervillenyc/status/1735827245384572940' target='_new'>arcade cabinet</a>" +
|
||||
|
||||
"Visit the <a href='https://x.com/wondervillenyc/status/1735827245384572940' target='_new'>arcade</a>"
|
||||
" <a href='https://www.tiktok.com/@indiearcadewave/video/7470192580500671786' target='_new'>cabinet</a>" +
|
||||
" 🕹️ at <a href='https://wonderville.nyc' target='_blank'>Wonderville</a> in Brooklyn, NY",
|
||||
|
||||
"Get the 📰️ <a href='press.html'>press kit</a> 📰️"
|
||||
];
|
||||
|
||||
|
2
lib/sb
@ -1 +1 @@
|
||||
Subproject commit 5a67b6e4967cacefc6ec4e40f1e1d03d681bd279
|
||||
Subproject commit 94070073d8921a9a9adb716a9b09db648b1ff526
|
94
resource/PT_Sans_License.txt
Normal file
@ -0,0 +1,94 @@
|
||||
Copyright (c) 2010, ParaType Ltd. (http://www.paratype.com/public),
|
||||
with Reserved Font Names "PT Sans" and "ParaType".
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
resource/PT_Sans_Narrow.ttf
Normal file
Before ![]() (image error) Size: 11 KiB |
Before ![]() (image error) Size: 23 KiB |
Before ![]() (image error) Size: 9.5 KiB |
BIN
resource/Steam_demo_message.png
Normal file
After ![]() (image error) Size: 42 KiB |
BIN
resource/bgm/Chapter1.ogg
Normal file
BIN
resource/bgm/Chapter2.ogg
Normal file
BIN
resource/bgm/Chapter3.ogg
Normal file
BIN
resource/bgm/Chapter4.ogg
Normal file
BIN
resource/check_mark.png
Normal file
After ![]() (image error) Size: 2.0 KiB |
Before ![]() (image error) Size: 4.0 KiB |
Before ![]() (image error) Size: 15 KiB |
Before ![]() (image error) Size: 41 KiB After ![]() (image error) Size: 46 KiB ![]() ![]() |
Before ![]() (image error) Size: 356 KiB |
BIN
resource/steam_button_sale.png
Normal file
After ![]() (image error) Size: 38 KiB |
BIN
resource/unchecked.png
Normal file
After ![]() (image error) Size: 2.8 KiB |
2143
src/Cakefoot.cpp
668
src/Cakefoot.hpp
@ -1,10 +1,10 @@
|
||||
/*@~@~@ C A K E F O O T <presented by> 💫dank.game💫
|
||||
|~)~)~)
|
||||
|\~*~*| Licensed under the zlib and CC-BY licenses. Source is available for modding at
|
||||
|\~*~*| Licensed under the zlib and CC-BY licenses. Source available for modding at
|
||||
|\\~*~|
|
||||
,~|#\\~*|~, <https://open.shampoo.ooo/shampoo/cakefoot>
|
||||
: \\@\\~| :
|
||||
: \\#\\| : Created with open SPACE🪐BOX engine for cross-platform, PC, web and mobile games
|
||||
: \\#\\| : Created with SPACE🪐BOX engine for cross-platform, PC, web and mobile games
|
||||
: \\@\' :
|
||||
: \\/ : <https://open.shampoo.ooo/shampoo/spacebox>
|
||||
`~ ~ ~`~ */
|
||||
@ -15,6 +15,7 @@
|
||||
#define GLM_ENABLE_EXPERIMENTAL
|
||||
|
||||
/* Standard library includes */
|
||||
#include <cstdlib>
|
||||
#include <stdlib.h>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
@ -27,11 +28,15 @@
|
||||
#endif
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <locale>
|
||||
#include <codecvt>
|
||||
#include <cerrno>
|
||||
|
||||
/* Include Game.hpp before any other SDL-related headers because it defines SDL_MAIN_HANDLED */
|
||||
#include "Game.hpp"
|
||||
|
||||
/* SPACEBOX external libraries included in source package */
|
||||
#include "SDL_mixer.h"
|
||||
#include "sdl2-gfx/SDL2_gfxPrimitives.h"
|
||||
#include "json/json.hpp"
|
||||
#include "glm/glm.hpp"
|
||||
@ -250,6 +255,525 @@ struct Splash
|
||||
float length;
|
||||
};
|
||||
|
||||
|
||||
/*!
|
||||
* Load multiple chunks into a single object, indexed by order loaded.
|
||||
*
|
||||
* Load each chunk by passing a file path. Each chunk loaded will be fully loaded into memory. Set the active chunk by
|
||||
* index, and all audio control functions will apply to the active chunk.
|
||||
*
|
||||
* When a multi-chunk is playing, and the active chunk is set to another chunk, the new chunk will start playing from
|
||||
* the beginning.
|
||||
*
|
||||
* All chunks are set to loop infinitely.
|
||||
*
|
||||
* When a multi-chunk is disabled, all chunks are disabled. The multi-chunk will need to be re-enabled to be able to
|
||||
* play any chunk.
|
||||
*/
|
||||
class MultiChunk
|
||||
{
|
||||
|
||||
std::vector<sb::audio::Chunk> chunks;
|
||||
std::size_t _index = 0;
|
||||
bool _enabled = true;
|
||||
|
||||
sb::audio::Chunk& current()
|
||||
{
|
||||
return chunks.at(_index);
|
||||
}
|
||||
|
||||
const sb::audio::Chunk& current() const
|
||||
{
|
||||
return chunks.at(_index);
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
/*!
|
||||
* Create an empty MultiChunk object.
|
||||
*/
|
||||
MultiChunk() = default;
|
||||
|
||||
/*!
|
||||
* @warning This loads the entire file into memory. It should only be called once per file, and it should not be
|
||||
* called on more files than can fit into memory. The memory will be unloaded automatically when this object is
|
||||
* destructed.
|
||||
*
|
||||
* @param path Path to an audio file to be loaded into memory and stored in this object
|
||||
*/
|
||||
void load(const fs::path& path)
|
||||
{
|
||||
sb::Log::Multi() << "Loading audio at " << path << sb::Log::end;
|
||||
sb::audio::Chunk chunk {path};
|
||||
chunk.loop();
|
||||
chunks.push_back(chunk);
|
||||
}
|
||||
|
||||
/*!
|
||||
* Set a chunk to active. If the chunk is already active, do nothing. If another chunk is currently playing, stop
|
||||
* that chunk and play the new one from the beginning.
|
||||
*
|
||||
* @param index The index of a chunk to set to active
|
||||
*
|
||||
* @throw std::out_of_range If the multi-chunk does not contain a chunk at the given index
|
||||
*/
|
||||
void set(std::size_t index)
|
||||
{
|
||||
if (index < chunks.size())
|
||||
{
|
||||
if (index != _index)
|
||||
{
|
||||
bool swap = current().playing();
|
||||
if (swap)
|
||||
{
|
||||
current().stop();
|
||||
}
|
||||
_index = index;
|
||||
if (swap)
|
||||
{
|
||||
current().play();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::ostringstream message;
|
||||
message << "Multi-chunk does not contain a chunk at index " << index;
|
||||
throw std::out_of_range(message.str());
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* Play the currently active chunk.
|
||||
*/
|
||||
void play()
|
||||
{
|
||||
current().play();
|
||||
}
|
||||
|
||||
/*!
|
||||
* Pause the currently active chunk.
|
||||
*/
|
||||
void pause()
|
||||
{
|
||||
current().pause();
|
||||
}
|
||||
|
||||
/*!
|
||||
* Stop the currently active chunk.
|
||||
*/
|
||||
void stop()
|
||||
{
|
||||
current().stop();
|
||||
}
|
||||
|
||||
/*!
|
||||
* Resume the currently active chunk.
|
||||
*/
|
||||
void resume()
|
||||
{
|
||||
current().resume();
|
||||
}
|
||||
|
||||
/*!
|
||||
* @return True if the current chunk is paused, false otherwise
|
||||
*/
|
||||
bool paused() const
|
||||
{
|
||||
return current().paused();
|
||||
}
|
||||
|
||||
/*!
|
||||
* @return True if the current chunk is playing, false otherwise
|
||||
*/
|
||||
bool playing() const
|
||||
{
|
||||
return current().playing();
|
||||
}
|
||||
|
||||
/*!
|
||||
* @return True if the current chunk is fading, false, otherwise
|
||||
*/
|
||||
bool fading() const
|
||||
{
|
||||
return current().fading();
|
||||
}
|
||||
|
||||
/*!
|
||||
* By default, a multi-chunk object is enabled to play audio. If disabled with this function, all chunks in the
|
||||
* multi-chunk will be disabled, and no audio will be able to be played. The multi-chunk will need to be enabled
|
||||
* again to be able to play any chunk.
|
||||
*
|
||||
* This function can be used both to set the state and to check the state.
|
||||
*
|
||||
* @param state Set to false to disable the multi-chunk, true to enable, or omit to check the current state
|
||||
* @return True if multi-chunk is set to enabled
|
||||
*/
|
||||
bool enabled(std::optional<bool> state = std::nullopt)
|
||||
{
|
||||
if (state.has_value())
|
||||
{
|
||||
_enabled = state.value();
|
||||
for (sb::audio::Chunk& chunk : chunks)
|
||||
{
|
||||
chunk.enabled(state);
|
||||
}
|
||||
}
|
||||
return _enabled;
|
||||
}
|
||||
|
||||
/*!
|
||||
* @return Index of the currently active chunk
|
||||
*/
|
||||
std::size_t index() const
|
||||
{
|
||||
return _index;
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
* A GUI widget which contains an editable string. Characters can be appended to the string one at a time using a
|
||||
* spinner widget which appears when edit mode is activated.
|
||||
*/
|
||||
class Textbox
|
||||
{
|
||||
|
||||
public:
|
||||
|
||||
/*!
|
||||
* Used to specify which characters are available to the textbox.
|
||||
*/
|
||||
struct Selection
|
||||
{
|
||||
static inline std::string uppercase { "uppercase" };
|
||||
static inline std::string decimal { "decimal" };
|
||||
static inline std::string all { "all" };
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
/* Display */
|
||||
sb::Sprite _sprite;
|
||||
sb::Text _text_plane;
|
||||
|
||||
/* Edit options */
|
||||
bool _editable = false;
|
||||
bool _editing = false;
|
||||
bool _allow_empty = false;
|
||||
std::string _selection = Selection::uppercase;
|
||||
std::size_t max = 0;
|
||||
|
||||
/* Next character input */
|
||||
std::shared_ptr<TTF_Font> _glyph_font;
|
||||
int character_index = 0;
|
||||
std::wstring character_set;
|
||||
sb::Sprite character_sprite;
|
||||
sb::Sprite up_arrow;
|
||||
sb::Sprite down_arrow;
|
||||
std::string original;
|
||||
float arrow_height = 0.5f;
|
||||
|
||||
/* Possible characters to input. Use wchar_t so that the backspace and submit glyphs can be stored in the same array
|
||||
* as the ASCII characters. The characters will be converted to UTF-8 when passed to sb::Text for display. */
|
||||
struct Characters
|
||||
{
|
||||
static inline std::wstring upper { L"ABCDEFGHIJKLMNOPQRSTUVWXYZ" };
|
||||
static inline std::wstring lower { L"abcdefghijklmnopqrstuvwxyz" };
|
||||
static inline std::wstring digits { L"0123456789" };
|
||||
static inline std::wstring decimal { digits + L"." };
|
||||
static inline std::wstring special { L" !#$%&'()*+,-./:;<=>?@[]^_`{|}~\"\\" };
|
||||
static inline wchar_t backspace { u'⇦' };
|
||||
static inline wchar_t submit { u'⇰' };
|
||||
};
|
||||
|
||||
/*!
|
||||
* @return The currently selected character
|
||||
*/
|
||||
wchar_t character() const
|
||||
{
|
||||
return character_set[character_index];
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
Textbox(const sb::Text& text_plane,
|
||||
const sb::Texture& up_arrow_texture,
|
||||
const sb::Texture& down_arrow_texture,
|
||||
std::string selection = Selection::uppercase,
|
||||
bool editable = false,
|
||||
bool allow_empty = false,
|
||||
std::size_t max = 0,
|
||||
float arrow_height = 0.5f) :
|
||||
_text_plane(text_plane), _editable(editable), _allow_empty(allow_empty), _selection(selection), max(max),
|
||||
up_arrow(up_arrow_texture), down_arrow(down_arrow_texture), arrow_height(arrow_height)
|
||||
{
|
||||
/* Create a sprite with the given text plane as the graphics */
|
||||
_sprite = sb::Sprite {text_plane};
|
||||
|
||||
/* Create a list of available characters for selection by storing them in a string. Include the backspace and
|
||||
* submit glyphs in the character list. */
|
||||
std::basic_ostringstream<wchar_t> set;
|
||||
if (selection == Selection::uppercase)
|
||||
{
|
||||
set << Characters::upper;
|
||||
}
|
||||
else if (selection == Selection::decimal)
|
||||
{
|
||||
set << Characters::decimal;
|
||||
}
|
||||
else if (selection == Selection::all)
|
||||
{
|
||||
set << Characters::upper << Characters::lower << Characters::digits << Characters::special;
|
||||
}
|
||||
set << Characters::backspace << Characters::submit;
|
||||
character_set = set.str();
|
||||
_selection = selection;
|
||||
|
||||
/* Create arrow sprites */
|
||||
up_arrow = sb::Sprite { up_arrow_texture };
|
||||
down_arrow = sb::Sprite { down_arrow_texture };
|
||||
}
|
||||
|
||||
void glyph_font(std::shared_ptr<TTF_Font> font)
|
||||
{
|
||||
_glyph_font = font;
|
||||
}
|
||||
|
||||
void text_plane(const sb::Text& text_plane)
|
||||
{
|
||||
_text_plane = text_plane;
|
||||
}
|
||||
|
||||
void editable(bool state = true)
|
||||
{
|
||||
_editable = state;
|
||||
}
|
||||
|
||||
void allow_empty(bool state = true)
|
||||
{
|
||||
_allow_empty = state;
|
||||
}
|
||||
|
||||
void translate(const glm::vec2& translation)
|
||||
{
|
||||
_sprite.translate(translation);
|
||||
}
|
||||
|
||||
void scale(float scale)
|
||||
{
|
||||
float ratio { float(_text_plane.dimensions().x) / _text_plane.dimensions().y };
|
||||
_sprite.scale({ratio * scale, scale});
|
||||
}
|
||||
|
||||
void edit()
|
||||
{
|
||||
_editing = true;
|
||||
|
||||
/* Store the content value so it can be reset if bad input is submitted */
|
||||
original = content();
|
||||
}
|
||||
|
||||
bool editing()
|
||||
{
|
||||
return _editing;
|
||||
}
|
||||
|
||||
void submit()
|
||||
{
|
||||
_editing = false;
|
||||
}
|
||||
|
||||
void content(const std::string message)
|
||||
{
|
||||
_text_plane.content(message);
|
||||
|
||||
/* If the string is empty, this object will just skip drawing the text in the draw function */
|
||||
if (!message.empty())
|
||||
{
|
||||
_text_plane.refresh();
|
||||
}
|
||||
|
||||
_sprite.plane(_text_plane);
|
||||
scale(_sprite.scale()[1]);
|
||||
}
|
||||
|
||||
const std::string& content() const
|
||||
{
|
||||
return _text_plane.content();
|
||||
}
|
||||
|
||||
void increment_character()
|
||||
{
|
||||
character_index++;
|
||||
|
||||
/* Wrap */
|
||||
if (character_index >= int(character_set.size()))
|
||||
{
|
||||
character_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void decrement_character()
|
||||
{
|
||||
character_index--;
|
||||
|
||||
/* Wrap */
|
||||
if (character_index < 0)
|
||||
{
|
||||
character_index = character_set.size() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* @return True if edits were submitted and the content changed, false otherwise
|
||||
*/
|
||||
bool press()
|
||||
{
|
||||
/* If not editing, toggle editing on */
|
||||
if (!_editing)
|
||||
{
|
||||
edit();
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Otherwise, submit the current glyph */
|
||||
else
|
||||
{
|
||||
/* Backspace was pressed, so remove the last character in the content */
|
||||
if (character() == Characters::backspace)
|
||||
{
|
||||
if (content().size() > 0)
|
||||
{
|
||||
content(content().substr(0, content().size() - 1));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Submit was pressed, stop editing and submit */
|
||||
else if (character() == Characters::submit)
|
||||
{
|
||||
_editing = false;
|
||||
|
||||
/* If the content is empty, restore the original value, unless empty has been allowed. */
|
||||
if (!_allow_empty && (content().size() == 0 || (_selection == Selection::decimal && content() == ".")))
|
||||
{
|
||||
content(original);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Clean up if there is no decimal or the decimal is at the beginning or end */
|
||||
else if (_selection == Selection::decimal)
|
||||
{
|
||||
std::string append;
|
||||
if (content().find('.') == std::string::npos)
|
||||
{
|
||||
append = ".0";
|
||||
}
|
||||
else if (content().find('.') == content().size() - 1)
|
||||
{
|
||||
append = "0";
|
||||
}
|
||||
if (append.size() > 0)
|
||||
{
|
||||
content(content() + append);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (content().find('.') == 0)
|
||||
{
|
||||
content("0" + content());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Content is submitted, so report whether it changed */
|
||||
return content() != original;
|
||||
}
|
||||
|
||||
/* Add a character to the content */
|
||||
else
|
||||
{
|
||||
/* Check the size is under the maximum */
|
||||
bool under_minimum { max == 0 || content().size() < max };
|
||||
|
||||
/* Check the user is not entering more than one decimal */
|
||||
bool single_decimal {
|
||||
character() != u'.' || _selection != Selection::decimal || content().find('.') == std::string::npos
|
||||
};
|
||||
|
||||
if (under_minimum && single_decimal)
|
||||
{
|
||||
std::string appended { content() };
|
||||
appended.push_back(character());
|
||||
content(appended);
|
||||
}
|
||||
|
||||
/* Content has not been submitted yet */
|
||||
return false;
|
||||
} } }
|
||||
|
||||
/*!
|
||||
* Draw text. If editing, draw the character input spinner.
|
||||
*
|
||||
* @param view Transformation from world space to camera space
|
||||
* @param projection Transformation from camera space to clip space
|
||||
* @param uniform List of uniforms available in the GLSL program
|
||||
*/
|
||||
void draw(
|
||||
const glm::mat4& view, const glm::mat4& projection, const std::map<std::string, GLuint>& uniform)
|
||||
{
|
||||
if (!content().empty())
|
||||
{
|
||||
_sprite.draw(uniform.at("mvp"), view, projection, uniform.at("texture enabled"));
|
||||
}
|
||||
|
||||
/* Draw the spinner to the right of the sprite. */
|
||||
if (_editing)
|
||||
{
|
||||
/* Prefer a glyph font, default to the same font as the text plane */
|
||||
std::shared_ptr<TTF_Font> font;
|
||||
if (_glyph_font.get() != nullptr)
|
||||
{
|
||||
font = _glyph_font;
|
||||
}
|
||||
else
|
||||
{
|
||||
font = _text_plane.font();
|
||||
}
|
||||
|
||||
/* Create a text plane with a single glyph. Convert the wchar_t character to a UTF-8 string, so sb::Text
|
||||
* will be able to display it. */
|
||||
sb::Text selection { font, "", _text_plane.foreground(), _text_plane.background() };
|
||||
std::string converted {
|
||||
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t>{}.to_bytes(character()) };
|
||||
selection.content(converted);
|
||||
selection.dimensions({_text_plane.dimensions().y, _text_plane.dimensions().y});
|
||||
selection.refresh();
|
||||
|
||||
/* Create a sprite for the character. Scale and translate it using the properties of the text plane. */
|
||||
character_sprite = sb::Sprite(selection);
|
||||
character_sprite.scale({_sprite.scale().y, _sprite.scale().y});
|
||||
character_sprite.translate(
|
||||
{_sprite.translation().x + _sprite.scale().x + character_sprite.scale().x,
|
||||
_sprite.translation().y});
|
||||
character_sprite.draw(uniform.at("mvp"), view, projection, uniform.at("texture enabled"));
|
||||
|
||||
/* Scale, translate, and draw the up and down arrows */
|
||||
glm::vec2 arrow_scale {character_sprite.scale().x, character_sprite.scale().y * arrow_height};
|
||||
up_arrow.scale(arrow_scale);
|
||||
up_arrow.translate({
|
||||
character_sprite.translation().x,
|
||||
_sprite.translation().y + _sprite.scale().y + arrow_scale.y});
|
||||
up_arrow.draw(uniform.at("mvp"), view, projection, uniform.at("texture enabled"));
|
||||
down_arrow.scale(arrow_scale);
|
||||
down_arrow.translate({
|
||||
character_sprite.translation().x,
|
||||
_sprite.translation().y - _sprite.scale().y - arrow_scale.y});
|
||||
down_arrow.draw(uniform.at("mvp"), view, projection, uniform.at("texture enabled"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
* The main game object. There is currently only support for one of these to exist at a time.
|
||||
*
|
||||
@ -323,31 +847,35 @@ private:
|
||||
std::map<std::string, std::shared_ptr<TTF_Font>> fonts {
|
||||
{"medium", font(configuration()("font", "medium", "path").get<std::string>(),
|
||||
configuration()("font", "medium", "size"))},
|
||||
{"small", font(configuration()("font", "small", "path").get<std::string>(),
|
||||
configuration()("font", "small", "size"))},
|
||||
{"large", font(configuration()("font", "large", "path").get<std::string>(),
|
||||
configuration()("font", "large", "size"))},
|
||||
{"glyph", font(configuration()("font", "glyph", "path").get<std::string>(),
|
||||
configuration()("font", "glyph", "size"))},
|
||||
{"glyph large", font(configuration()("font", "glyph large", "path").get<std::string>(),
|
||||
configuration()("font", "glyph large", "size"))}
|
||||
configuration()("font", "glyph large", "size"))},
|
||||
{"narrow medium", font(configuration()("font", "narrow medium", "path").get<std::string>(),
|
||||
configuration()("font", "narrow medium", "size"))},
|
||||
{"narrow progress menu", font(configuration()("font", "narrow progress menu", "path").get<std::string>(),
|
||||
configuration()("font", "narrow progress menu", "size"), std::nullopt,
|
||||
TTF_WRAPPED_ALIGN_CENTER)}
|
||||
};
|
||||
std::map<std::string, sb::Text> label = {
|
||||
{"fps", sb::Text(font())},
|
||||
{"clock", sb::Text(font())},
|
||||
{"level", sb::Text(font())},
|
||||
{"level select", sb::Text(font())},
|
||||
{"profile", sb::Text(font())},
|
||||
{"challenge", sb::Text(font())},
|
||||
{"view", sb::Text(font())},
|
||||
{"level select", sb::Text(fonts.at("narrow medium"))},
|
||||
{"profile", sb::Text(fonts.at("narrow medium"))},
|
||||
{"challenge", sb::Text(fonts.at("narrow medium"))},
|
||||
{"view", sb::Text(fonts.at("narrow medium"))},
|
||||
{"game over", sb::Text(font())},
|
||||
{"arcade rank", sb::Text(fonts.at("large"))},
|
||||
{"arcade distance", sb::Text(fonts.at("large"))},
|
||||
{"quest best", sb::Text(fonts.at("glyph"))},
|
||||
{"idle warning", sb::Text(font())},
|
||||
{"version", sb::Text(font())}
|
||||
{"version", sb::Text(font())},
|
||||
{"credits available", sb::Text(fonts.at(configuration()("arcade", "credits available", "font")))}
|
||||
};
|
||||
sb::Sprite playing_field, checkpoint_on, checkpoint_off, qr_code, qr_code_bg, social, auto_save, demo_message,
|
||||
sb::Sprite playing_field, checkpoint_on, checkpoint_off, qr_code, qr_code_bg, auto_save, demo_message,
|
||||
coin {configuration()("texture", "coin").get<std::string>(), glm::vec2{12.0f / 486.0f}, GL_LINEAR};
|
||||
sb::Timer on_timer, run_timer, unpaused_timer, idle_timer, survival_timer, stray_timer, continuous_timer;
|
||||
glm::vec3 camera_position {0.0f, 0.0f, 2.0f}, subject_position {0.0f, 0.0f, 0.0f};
|
||||
@ -356,7 +884,6 @@ private:
|
||||
std::vector<Curve> curves;
|
||||
std::vector<std::shared_ptr<Enemy>> enemies;
|
||||
glm::vec4 world_color {0.2f, 0.2f, 0.2f, 1.0f};
|
||||
std::map<std::string, sb::audio::Chunk> audio;
|
||||
Character character {_configuration, audio};
|
||||
bool use_play_button = false, coin_collected = false, blinking_visible = true, arcade_limit_warning = false,
|
||||
coin_returned = false, let_go = false, icing_available = false;
|
||||
@ -375,6 +902,7 @@ private:
|
||||
sb::progress::Stats stats;
|
||||
sb::progress::Achievements achievements;
|
||||
std::optional<float> previous_distance;
|
||||
sb::Texture increment_texture, decrement_texture;
|
||||
|
||||
/* Vector of coin sprite objects to be drawn to the screen simultaneously */
|
||||
std::vector<sb::Sprite> bank_ui;
|
||||
@ -599,7 +1127,7 @@ private:
|
||||
|
||||
/* This animation can be used to end the game over state after the time limit is reached. Play once with a delay to let the
|
||||
* game over screen display temporarily before being ended by this animation. */
|
||||
Animation game_over_animation {std::bind(&Cakefoot::end_game_over_display, this)};
|
||||
sb::Animation game_over_animation {std::bind(&Cakefoot::end_game_over_display, this)};
|
||||
|
||||
/*!
|
||||
* Write score, refresh scoreboard, and load the title screen
|
||||
@ -607,7 +1135,7 @@ private:
|
||||
void submit_score();
|
||||
|
||||
/* Can be used to time out the name entry screen */
|
||||
Animation submit_score_animation {std::bind(&Cakefoot::submit_score, this)};
|
||||
sb::Animation submit_score_animation {std::bind(&Cakefoot::submit_score, this)};
|
||||
|
||||
/*!
|
||||
*/
|
||||
@ -619,10 +1147,10 @@ private:
|
||||
void shift_hue();
|
||||
|
||||
/* Shift the hue by the configured amount once per configured amount of seconds */
|
||||
Animation shift_hue_animation {std::bind(&Cakefoot::shift_hue, this)};
|
||||
sb::Animation shift_hue_animation {std::bind(&Cakefoot::shift_hue, this)};
|
||||
|
||||
/* Flash the screen */
|
||||
Animation flash_animation;
|
||||
sb::Animation flash_animation;
|
||||
|
||||
/*!
|
||||
* Toggle visibility of the flag used by the blink animation.
|
||||
@ -630,10 +1158,10 @@ private:
|
||||
void blink();
|
||||
|
||||
/* Toggle visibility flag every interval */
|
||||
Animation blink_animation {std::bind(&Cakefoot::blink, this), configuration()("display", "blink frequency")};
|
||||
sb::Animation blink_animation {std::bind(&Cakefoot::blink, this), configuration()("display", "blink frequency")};
|
||||
|
||||
/* Count a cooldown period for gamepad axes */
|
||||
Animation cooldown_animation;
|
||||
sb::Animation cooldown_animation;
|
||||
|
||||
/*!
|
||||
* Display next splash image.
|
||||
@ -641,7 +1169,7 @@ private:
|
||||
void next_splash();
|
||||
|
||||
/* Display splash images in succession until all splash images have been displayed. */
|
||||
Animation splash_animation {std::bind(&Cakefoot::next_splash, this)};
|
||||
sb::Animation splash_animation {std::bind(&Cakefoot::next_splash, this)};
|
||||
|
||||
/*!
|
||||
* Set arcade time limit warning state at Cakefoot::arcade_limit_warning based on mode, time remaining, and whether
|
||||
@ -650,13 +1178,13 @@ private:
|
||||
void flash_warning();
|
||||
|
||||
/* Test whether or not the arcade time limit warning should be active. */
|
||||
Animation warning_animation {std::bind(&Cakefoot::flash_warning, this)};
|
||||
sb::Animation warning_animation {std::bind(&Cakefoot::flash_warning, this)};
|
||||
|
||||
/* Reset booleans on a timer to indicate stat write and sync is needed. */
|
||||
bool save_stats = false;
|
||||
bool sync_stats = false;
|
||||
Animation save_stats_animation {[&](){ save_stats = true; }};
|
||||
Animation sync_stats_animation {[&](){ sync_stats = true; }};
|
||||
sb::Animation save_stats_animation {[&](){ save_stats = true; }};
|
||||
sb::Animation sync_stats_animation {[&](){ sync_stats = true; }};
|
||||
|
||||
/*!
|
||||
* Get the arcade time as the amount of time remaining before the limit is reached.
|
||||
@ -726,6 +1254,85 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* @return The maximum index of the challenge list
|
||||
*/
|
||||
int max_challenge() const;
|
||||
|
||||
sb::Sprite achievements_background;
|
||||
sb::Color achievements_background_color;
|
||||
std::vector<sb::Sprite> achievements_text_sprites;
|
||||
|
||||
/*!
|
||||
* Create new text sprite graphics for each achievement on the menu. Any existing graphics will be cleared.
|
||||
*/
|
||||
void load_achievements_menu();
|
||||
|
||||
/*!
|
||||
* Draw all text sprite graphics and background for displaying the achievements menu.
|
||||
*
|
||||
* @param view Transformation from world space to camera space
|
||||
* @param projection Transformation from camera space to clip space
|
||||
* @param uniform List of uniforms available in the GLSL program
|
||||
*/
|
||||
void draw_achievements(
|
||||
const glm::mat4& view, const glm::mat4& projection, const std::map<std::string, GLuint>& uniform) const;
|
||||
|
||||
sb::Sprite stats_sprite;
|
||||
|
||||
/*!
|
||||
* Create a new texture with text displaying all the stats, replacing any existing graphics.
|
||||
*/
|
||||
void load_stats_menu();
|
||||
|
||||
/*!
|
||||
* Draw the text sprite that displays the stats menu.
|
||||
*
|
||||
* @param view Transformation from world space to camera space
|
||||
* @param projection Transformation from camera space to clip space
|
||||
* @param uniform List of uniforms available in the GLSL program
|
||||
*/
|
||||
void draw_stats(
|
||||
const glm::mat4& view, const glm::mat4& projection, const std::map<std::string, GLuint>& uniform) const;
|
||||
|
||||
std::string achievements_pop_up_text;
|
||||
sb::Sprite achievements_pop_up_sprite;
|
||||
sb::Animation achievements_pop_up_animation;
|
||||
|
||||
/*!
|
||||
* Create a sprite for a pop-up showing the text stored in Cakefoot::achievements_pop_up_text. Existing graphics
|
||||
* will be overwritten.
|
||||
*/
|
||||
void load_achievements_pop_up();
|
||||
|
||||
std::map<std::string, sb::Sprite> operator_menu_labels;
|
||||
std::map<std::string, sb::Pad<>> operator_menu_buttons;
|
||||
std::map<std::string, Textbox> operator_menu_textboxes;
|
||||
bool operator_menu_edited = false;
|
||||
int operator_menu_index_selected = 0;
|
||||
bool operator_menu_confirming = false;
|
||||
|
||||
/*!
|
||||
* Create UI objects for the operator menu.
|
||||
*
|
||||
* Set the preserve flag to keep the current state and contents of each widget. Otherwise, the state will be reset
|
||||
* according to values in the "operator" section of the config. This flag can be used to preserve the state of
|
||||
* checkboxes and text without changing the config value.
|
||||
*
|
||||
* @param preserve Keep the current state and contents of each widget
|
||||
*/
|
||||
void load_operator_menu(bool preserve = false);
|
||||
|
||||
/*!
|
||||
* Draw the operator menu UI.
|
||||
*
|
||||
* @param view Transformation from world space to camera space
|
||||
* @param projection Transformation from camera space to clip space
|
||||
* @param uniform List of uniforms available in the GLSL program
|
||||
*/
|
||||
void draw_operator_menu(
|
||||
const glm::mat4& view, const glm::mat4& projection, const std::map<std::string, GLuint>& uniform);
|
||||
|
||||
protected:
|
||||
|
||||
/* Flag indicating whether or not to turn off collisions. Intended for automated testing of the game, using a mock
|
||||
@ -733,6 +1340,9 @@ protected:
|
||||
* this on in any of its functions. */
|
||||
bool noclip = false;
|
||||
|
||||
/* Make this flag available to tests. */
|
||||
bool operator_menu_active = false;
|
||||
|
||||
/*!
|
||||
* Unlock the date-based achievements ACH_CAKE_MY_DAY and ACH_BIRTHDAY_CAKE if the given date is valid.
|
||||
*
|
||||
@ -762,6 +1372,20 @@ protected:
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* Respond to a change in the configuration.
|
||||
*/
|
||||
void reconfig();
|
||||
|
||||
/*!
|
||||
* Make the audio available to mocks for testing.
|
||||
*/
|
||||
std::map<std::string, sb::audio::Chunk> audio;
|
||||
MultiChunk bgm;
|
||||
|
||||
/* Make credit count available to mocks for testing. */
|
||||
float credits = 0.0f;
|
||||
|
||||
public:
|
||||
|
||||
/*!
|
||||
|
@ -75,8 +75,8 @@ void Character::spawn(const Curve& curve, bool continue_acceleration)
|
||||
accelerating = false;
|
||||
}
|
||||
_resting = true;
|
||||
audio.at("walk").stop();
|
||||
audio.at("reverse").stop();
|
||||
audio.at("walk").stop(0.5f);
|
||||
audio.at("reverse").stop(0.5f);
|
||||
}
|
||||
|
||||
void Character::checkpoint(float checkpoint)
|
||||
@ -120,7 +120,8 @@ float Character::speed() const
|
||||
return _speed;
|
||||
}
|
||||
|
||||
void Character::update(const Curve& curve, const sb::Timer& timer, bool muted, std::optional<float> constant_speed)
|
||||
void Character::update(
|
||||
const Curve& curve, const sb::Timer& timer, const glm::vec3& ndc, bool muted, std::optional<float> constant_speed)
|
||||
{
|
||||
if (timer.frame() > 0.0f)
|
||||
{
|
||||
@ -154,8 +155,7 @@ void Character::update(const Curve& curve, const sb::Timer& timer, bool muted, s
|
||||
float min_speed = timer.delta(profile()["min speed"].get<float>());
|
||||
_speed = std::clamp(_speed, min_speed, max_speed);
|
||||
|
||||
/* Calculate volume based on speed relative to max speed */
|
||||
int volume;
|
||||
/* Play walking effects and pan to the left and right based on screen position */
|
||||
if (!constant_speed.has_value())
|
||||
{
|
||||
if (_speed >= 0.0f)
|
||||
@ -164,14 +164,14 @@ void Character::update(const Curve& curve, const sb::Timer& timer, bool muted, s
|
||||
audio.at("reverse").stop();
|
||||
if (!audio.at("walk").playing())
|
||||
{
|
||||
audio.at("walk").play(0.0f, walk_channel);
|
||||
audio.at("walk").play(0.7f, walk_channel);
|
||||
sb::audio::pan_channel(walk_channel, ndc.x);
|
||||
}
|
||||
|
||||
if (!muted)
|
||||
{
|
||||
/* Get louder closer to max speed using an exponential scale */
|
||||
volume = std::round(std::pow(_speed / max_speed, 3.0f) * static_cast<float>(MIX_MAX_VOLUME));
|
||||
audio.at("walk").channel_volume(volume);
|
||||
/* Pan based on X position on screen */
|
||||
sb::audio::pan_channel(walk_channel, ndc.x);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -181,17 +181,17 @@ void Character::update(const Curve& curve, const sb::Timer& timer, bool muted, s
|
||||
else
|
||||
{
|
||||
/* Only play walking backward effect. */
|
||||
audio.at("walk").stop();
|
||||
audio.at("walk").stop(0.1f);
|
||||
if (!audio.at("reverse").playing())
|
||||
{
|
||||
audio.at("reverse").play(0.0f, reverse_channel);
|
||||
audio.at("reverse").play(0.7f, reverse_channel);
|
||||
sb::audio::pan_channel(reverse_channel, ndc.x);
|
||||
}
|
||||
|
||||
if (!muted)
|
||||
{
|
||||
/* Get louder closer to min speed using an exponential scale */
|
||||
volume = std::round(std::pow(_speed / min_speed, 3.0f) * static_cast<float>(MIX_MAX_VOLUME));
|
||||
audio.at("reverse").channel_volume(volume);
|
||||
/* Pan based on X position on screen */
|
||||
sb::audio::pan_channel(reverse_channel, ndc.x);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -176,12 +176,13 @@ public:
|
||||
/*!
|
||||
* Check acceleration state and adjust speed. Move character toward the next point on the curve.
|
||||
*
|
||||
* @param curve the curve to update against
|
||||
* @param timer a timer object that is updated once per frame, so that it provides delta time for movement
|
||||
* @param muted flag for preventing the walk sound effect output
|
||||
* @param constant_speed override acceleration processing and set the movement to a constant speed
|
||||
* @param curve The curve to update against
|
||||
* @param timer A timer object that is updated once per frame, so that it provides delta time for movement
|
||||
* @param ndc Coordinates of the character in NDC space
|
||||
* @param muted Flag for preventing the walk sound effect output
|
||||
* @param constant_speed Override acceleration processing and set the movement to a constant speed
|
||||
*/
|
||||
void update(const Curve& curve, const sb::Timer& timer, bool muted = false,
|
||||
void update(const Curve& curve, const sb::Timer& timer, const glm::vec3& ndc, bool muted = false,
|
||||
std::optional<float> constant_speed = std::nullopt);
|
||||
|
||||
/*!
|
||||
|
15
src/cakefoot.service
Normal file
@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=Cakefoot is a single-button dodge 'em up arcade game on rails - fork it up!
|
||||
After=multi-user.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=%h/cakefoot/Cakefoot-linux
|
||||
Environment=PWD=%h/cakefoot/Cakefoot-linux
|
||||
ExecStart=%h/cakefoot/Cakefoot-linux/Cakefoot-linux.x64
|
||||
Restart=always
|
||||
RestartSec=30sec
|
||||
TimeoutStartSec=2min
|
||||
StandardOutput=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
@ -4,14 +4,16 @@
|
||||
"dimensions": [1920, 1080],
|
||||
"show cursor": false,
|
||||
"fullscreen": true,
|
||||
"arcade only": true,
|
||||
"use arcade prompt": true,
|
||||
"qr display": true,
|
||||
"social media visible": false,
|
||||
"steam button visible": true,
|
||||
"dank logo visible": true
|
||||
},
|
||||
|
||||
"arcade":
|
||||
{
|
||||
"arcade only": true
|
||||
},
|
||||
|
||||
"button":
|
||||
{
|
||||
"steam translation": [-1.35, -0.55],
|
||||
|
28
src/config_arcade_cabinet.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"arcade":
|
||||
{
|
||||
"credits enabled": true,
|
||||
"credits required": 1.0
|
||||
},
|
||||
|
||||
"operator":
|
||||
{
|
||||
"enabled": true
|
||||
},
|
||||
|
||||
"system":
|
||||
{
|
||||
"daemon service name": "cakefoot",
|
||||
"wi-fi network": "",
|
||||
"wi-fi password": "",
|
||||
"update":
|
||||
{
|
||||
"automatic": true,
|
||||
"location": "https://cakefoot.dank.game/dist/protected/",
|
||||
"info file": "Cakefoot-linux_arcade-latest.json",
|
||||
"working directory": "~/cakefoot",
|
||||
"symlink": "Cakefoot-linux",
|
||||
"timeout": 30
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,6 @@
|
||||
["resource/splash_coolmath.png", [36.0, 36.0, 36.0, 255.0], 2.0]
|
||||
],
|
||||
"default initials": "CMG",
|
||||
"name entry enabled": false,
|
||||
"social media click": false
|
||||
"name entry enabled": false
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,10 @@
|
||||
{
|
||||
"display":
|
||||
{
|
||||
"dimensions": [1280, 720],
|
||||
"vsync": false
|
||||
},
|
||||
|
||||
"recording":
|
||||
{
|
||||
"enabled": false
|
||||
@ -9,7 +15,7 @@
|
||||
"stdout enabled": true,
|
||||
"file enabled": true,
|
||||
"debug to file": true,
|
||||
"debug to stdout": true,
|
||||
"debug to stdout": false,
|
||||
"output directory": "local/log",
|
||||
"gl error": true
|
||||
},
|
||||
@ -27,5 +33,15 @@
|
||||
{
|
||||
"stats write frequency": 5.0,
|
||||
"steam stats sync frequency": 120.0
|
||||
},
|
||||
|
||||
"audio":
|
||||
{
|
||||
"test enabled": true
|
||||
},
|
||||
|
||||
"configuration":
|
||||
{
|
||||
"auto refresh": true
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,6 @@
|
||||
"display":
|
||||
{
|
||||
"qr display": true,
|
||||
"social media visible": false,
|
||||
"steam button visible": true,
|
||||
"dank logo visible": true,
|
||||
"show cursor": false
|
||||
|
16
src/config_steam_demo.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"display":
|
||||
{
|
||||
"auto save translation": [-999.0, -999.0]
|
||||
},
|
||||
|
||||
"demo":
|
||||
{
|
||||
"active": true
|
||||
},
|
||||
|
||||
"texture":
|
||||
{
|
||||
"demo message": "resource/Steam_demo_message.png"
|
||||
}
|
||||
}
|
11
src/config_steam_sale.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"button":
|
||||
{
|
||||
"steam ratio": 0.2681
|
||||
},
|
||||
|
||||
"texture":
|
||||
{
|
||||
"steam button": "resource/steam_button_sale.png"
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@
|
||||
"fluid resize": false,
|
||||
"use play button": true,
|
||||
"scoreboard wrap": 3200,
|
||||
"social media click": false,
|
||||
"title": "Cakefoot 🍰😈 | Rage game | Play Online!",
|
||||
"steam button visible": true,
|
||||
"exit enabled": false,
|
||||
@ -25,7 +24,7 @@
|
||||
|
||||
"log":
|
||||
{
|
||||
"stdout enabled": true,
|
||||
"stdout enabled": false,
|
||||
"file enabled": false
|
||||
},
|
||||
|
||||
|
39
src/main.cpp
@ -1,13 +1,13 @@
|
||||
/*@~@~@ | C A K E F O O T <presented by> 💫dank.game💫
|
||||
|~)~)~) |
|
||||
|\~*~*| | Licensed under the zlib and CC-BY licenses. Source is available for modding at
|
||||
|\\~*~| |
|
||||
|#\\~*| | <https://open.shampoo.ooo/shampoo/cakefoot>
|
||||
\\@\\~| |
|
||||
\\#\\| | Created with open SPACE🪐BOX engine for cross-platform, PC, web and mobile games
|
||||
\\@\' |
|
||||
\\/ | <https://open.shampoo.ooo/shampoo/spacebox>
|
||||
------`---*/
|
||||
/*@~@~@ C A K E F O O T <presented by> 💫dank.game💫
|
||||
|~)~)~)
|
||||
|\~*~*| Licensed under the zlib and CC-BY licenses. Source is available at
|
||||
|\\~*~|
|
||||
,~|#\\~*|~, <https://open.shampoo.ooo/shampoo/cakefoot>
|
||||
: \\@\\~| :
|
||||
: \\#\\| : Created with open SPACE🪐BOX engine for cross-platform, PC, web and mobile games
|
||||
: \\@\' :
|
||||
: \\/ : <https://open.shampoo.ooo/shampoo/spacebox>
|
||||
`~ ~ ~`~ */
|
||||
|
||||
#include "main.hpp"
|
||||
|
||||
@ -19,25 +19,6 @@ int main(int argc, char** argv)
|
||||
* configuration, one at a time, in the order passed to the game's constructor. */
|
||||
std::vector<nlohmann::json> additional_config;
|
||||
|
||||
/* Handle builds that expect a special configuration */
|
||||
#if defined(__ARCADE_ONLY__)
|
||||
/* Add the arcade-only configuration */
|
||||
nlohmann::json arcade_config = sb::json_from_file("src/config_arcade.json");
|
||||
additional_config.push_back(arcade_config);
|
||||
#elif defined(__DEMO__)
|
||||
/* Add the demo configuration */
|
||||
nlohmann::json demo_config = sb::json_from_file("src/config_demo.json");
|
||||
additional_config.push_back(demo_config);
|
||||
#elif defined(__COOLMATH__)
|
||||
/* Merge config specific to the coolmath WASM build */
|
||||
nlohmann::json coolmath_config = sb::json_from_file("src/config_coolmath.json");
|
||||
additional_config.push_back(coolmath_config);
|
||||
#elif defined(__ITCH__)
|
||||
/* Merge config specific to the itch.io WASM build */
|
||||
nlohmann::json itch_config = sb::json_from_file("src/config_itch.json");
|
||||
additional_config.push_back(itch_config);
|
||||
#endif
|
||||
|
||||
/* Parse the command line */
|
||||
try
|
||||
{
|
||||
|
6
src/polkit_network_manager_rule.js
Normal file
@ -0,0 +1,6 @@
|
||||
polkit.addRule(function(action, subject) {
|
||||
if (action.id.startsWith("org.freedesktop.NetworkManager") &&
|
||||
(subject.isInGroup("sudo") || subject.isInGroup("netdev"))) {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
167
src/release.py
@ -1,12 +1,13 @@
|
||||
# /~~~~~.
|
||||
# |\~~~~: [ C A K E F O O T ] presented by dank.game
|
||||
# |\\~~~|
|
||||
# |#\\~~: open source code and license details at https://open.shampoo.ooo/shampoo/cakefoot
|
||||
# \\#\\~|
|
||||
# \\#\\: created using the SPACE🪐BOX engine https://open.shampoo.ooo/shampoo/spacebox
|
||||
# \\#\'
|
||||
# \\#|
|
||||
# `-'
|
||||
# /~@~@~@ C A K E F O O T <presented by> 💫dank.game💫
|
||||
# |~)~)~)
|
||||
# |\~*~*| Licensed under the zlib and CC-BY licenses. Source available for modding at
|
||||
# |\\~*~|
|
||||
# ,~|#\\~*|~, <https://open.shampoo.ooo/shampoo/cakefoot>
|
||||
# : \\@\\~| :
|
||||
# : \\#\\| : Created with SPACE🪐BOX engine for cross-platform, PC, web and mobile games
|
||||
# : \\@\' :
|
||||
# : \\/ : <https://open.shampoo.ooo/shampoo/spacebox>
|
||||
# `~ ~ ~`~ */
|
||||
#
|
||||
# This script is used to add new releases to the distributions folder. The distributions folder is a separate
|
||||
# folder from the builds folder where specific versions of builds are "frozen" to an arbitrary tag or release,
|
||||
@ -45,54 +46,66 @@ if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Copy current builds in the build directory into dist for release, with a given tag applied.",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument("tag", help="The tag to be appended to the file name, for example, '4.20.69'.")
|
||||
parser.add_argument("--build", type=pathlib.Path, default=pathlib.Path("build/"), help="The location of per-platform builds")
|
||||
|
||||
parser.add_argument(
|
||||
"--dist", type=pathlib.Path, default=pathlib.Path("dist/"), help="The location where distributable builds are kept")
|
||||
parser.add_argument("--project", default="Cakefoot", help="This project name will be prepended to the dist archive files")
|
||||
"tag",
|
||||
help="The tag to be appended to the file name, for example, '1.1.5'.")
|
||||
|
||||
parser.add_argument(
|
||||
"--unprotected", action="store_true",
|
||||
"--build",
|
||||
type=pathlib.Path, default=pathlib.Path("build/"),
|
||||
help="The location of per-platform builds")
|
||||
|
||||
parser.add_argument(
|
||||
"--dist",
|
||||
type=pathlib.Path,
|
||||
default=pathlib.Path("dist/"),
|
||||
help="The location where distributable builds are kept")
|
||||
|
||||
parser.add_argument(
|
||||
"--project",
|
||||
default="Cakefoot",
|
||||
help="This project name will be prepended to the dist archive files")
|
||||
|
||||
parser.add_argument(
|
||||
"--unprotected",
|
||||
action="store_true",
|
||||
help="Unless this flag is passed, non-WASM builds will be copied to a folder in dist named 'protected'")
|
||||
|
||||
arguments = parser.parse_args()
|
||||
|
||||
# WASM build. Original Bash commands:
|
||||
#
|
||||
# mkdir -p dist/wasm-$TAG
|
||||
# cp build/wasm/cakefoot.* dist/wasm-$TAG
|
||||
# ln -nsf wasm-$TAG dist/wasm
|
||||
# WASM
|
||||
|
||||
wasm_build_dir = arguments.build / "wasm"
|
||||
wasm_dist_name = f"wasm-{arguments.tag}"
|
||||
wasm_dist_dir = arguments.dist / wasm_dist_name
|
||||
os.makedirs(wasm_dist_dir, mode=0o755, exist_ok=True)
|
||||
for path in glob.glob(str(wasm_build_dir / "cakefoot.*")):
|
||||
go_for_it = True
|
||||
if (wasm_dist_dir / os.path.basename(path)).exists():
|
||||
response = input(f"Replace existing file at {wasm_dist_dir / path}? [y/n]: ")
|
||||
if wasm_build_dir.exists():
|
||||
source_paths = glob.glob(str(wasm_build_dir / "Cakefoot.*"))
|
||||
if source_paths:
|
||||
wasm_dist_name = f"wasm-{arguments.tag}"
|
||||
wasm_dist_dir = arguments.dist / wasm_dist_name
|
||||
os.makedirs(wasm_dist_dir, mode=0o755, exist_ok=True)
|
||||
for path in source_paths:
|
||||
go_for_it = True
|
||||
if (wasm_dist_dir / os.path.basename(path)).exists():
|
||||
response = input(f"Replace existing file at {wasm_dist_dir / path}? [y/n]: ")
|
||||
if response.lower() != "y":
|
||||
go_for_it = False
|
||||
if go_for_it:
|
||||
shutil.copy(path, wasm_dist_dir)
|
||||
print(f"Copied {path} to {wasm_dist_dir}")
|
||||
wasm_link = arguments.dist / "wasm"
|
||||
go_for_it = True
|
||||
response = input(f"Replace existing link at {wasm_link}? [y/n]: ")
|
||||
if response.lower() != "y":
|
||||
go_for_it = False
|
||||
if go_for_it:
|
||||
shutil.copy(path, wasm_dist_dir)
|
||||
print(f"Copied {path} to {wasm_dist_dir}")
|
||||
wasm_link = arguments.dist / "wasm"
|
||||
go_for_it = True
|
||||
response = input(f"Replace existing link at {wasm_link}? [y/n]: ")
|
||||
if response.lower() != "y":
|
||||
go_for_it = False
|
||||
if go_for_it:
|
||||
force_link(wasm_dist_name, wasm_link, True)
|
||||
print(f"Linked {wasm_link} to {wasm_dist_dir}")
|
||||
if go_for_it:
|
||||
force_link(wasm_dist_name, wasm_link, True)
|
||||
print(f"Linked {wasm_link} to {wasm_dist_dir}")
|
||||
else:
|
||||
print(f"Error: no files to copy in {wasm_build_dir}")
|
||||
else:
|
||||
print(f"Error: no build directory found at {wasm_build_dir}")
|
||||
|
||||
# Downloadable builds. Original Bash commands:
|
||||
#
|
||||
# cp build/ubuntu18/Cakefoot-linux.zip dist/protected/Cakefoot-linux-$TAG.zip
|
||||
# ln -nsf Cakefoot-linux-$TAG.zip dist/protected/Cakefoot-linux.zip
|
||||
# cp build/win32/Cakefoot-win32.zip dist/protected/Cakefoot-win32-$TAG.zip
|
||||
# ln -nsf Cakefoot-win32-$TAG.zip dist/protected/Cakefoot-win32.zip
|
||||
# cp build/win64/Cakefoot-win64.zip dist/protected/Cakefoot-win64-$TAG.zip
|
||||
# ln -nsf Cakefoot-win64-$TAG.zip dist/protected/Cakefoot-win64.zip
|
||||
# cp build/macos/Cakefoot.dmg dist/protected/Cakefoot-$TAG.dmg
|
||||
# ln -nsf Cakefoot-$TAG.dmg dist/protected/Cakefoot.dmg
|
||||
# Downloadable
|
||||
|
||||
distributions = {
|
||||
"ubuntu18": ["linux", "zip"],
|
||||
@ -116,35 +129,41 @@ if __name__ == "__main__":
|
||||
dist_name += f"-{arguments.tag}.{name[1]}"
|
||||
link_name += f".{name[1]}"
|
||||
source = arguments.build / platform / build_name
|
||||
destination = dist_dir / dist_name
|
||||
go_for_it = True
|
||||
if destination.exists():
|
||||
response = input(f"Replace existing file at {destination}? [y/n]: ")
|
||||
if response.lower() != "y":
|
||||
go_for_it = False
|
||||
if go_for_it:
|
||||
shutil.copy(source, destination)
|
||||
print(f"Copied {source} to {destination}")
|
||||
link_path = dist_dir / link_name
|
||||
go_for_it = True
|
||||
if link_path.exists():
|
||||
response = input(f"Replace existing link at {link_path}? [y/n]: ")
|
||||
if response.lower() != "y":
|
||||
go_for_it = False
|
||||
if go_for_it:
|
||||
force_link(dist_name, link_path)
|
||||
print(f"Linked {link_path} to {destination}")
|
||||
if source.exists():
|
||||
destination = dist_dir / dist_name
|
||||
go_for_it = True
|
||||
if destination.exists():
|
||||
response = input(f"Replace existing file at {destination}? [y/n]: ")
|
||||
if response.lower() != "y":
|
||||
go_for_it = False
|
||||
if go_for_it:
|
||||
shutil.copy(source, destination)
|
||||
print(f"Copied {source} to {destination}")
|
||||
link_path = dist_dir / link_name
|
||||
go_for_it = True
|
||||
if link_path.exists():
|
||||
response = input(f"Replace existing link at {link_path}? [y/n]: ")
|
||||
if response.lower() != "y":
|
||||
go_for_it = False
|
||||
if go_for_it:
|
||||
force_link(dist_name, link_path)
|
||||
print(f"Linked {link_path} to {destination}")
|
||||
else:
|
||||
print(f"Error: {platform} build {source} is missing.")
|
||||
|
||||
# WASM archives
|
||||
|
||||
for platform in ("coolmath", "itch"):
|
||||
build_path = arguments.build / f"wasm_{platform}" / f"{arguments.project}_{platform}.zip"
|
||||
dist_path = arguments.dist / f"{arguments.project}_{platform}-{arguments.tag}.zip"
|
||||
go_for_it = True
|
||||
if dist_path.exists():
|
||||
response = input(f"Replace existing file at {dist_path}? [y/n]: ")
|
||||
if response.lower() != "y":
|
||||
go_for_it = False
|
||||
if go_for_it:
|
||||
shutil.copy(build_path, dist_path)
|
||||
print(f"Copied {build_path} to {dist_path}")
|
||||
if build_path.exists():
|
||||
dist_path = arguments.dist / f"{arguments.project}_{platform}-{arguments.tag}.zip"
|
||||
go_for_it = True
|
||||
if dist_path.exists():
|
||||
response = input(f"Replace existing file at {dist_path}? [y/n]: ")
|
||||
if response.lower() != "y":
|
||||
go_for_it = False
|
||||
if go_for_it:
|
||||
shutil.copy(build_path, dist_path)
|
||||
print(f"Copied {build_path} to {dist_path}")
|
||||
else:
|
||||
print("Error: {platform} build file is missing at {build_path}")
|
||||
|
@ -42,7 +42,6 @@ nlohmann::json mock_progress_json = R"({
|
||||
"current level": 7,
|
||||
"current view": 2,
|
||||
"jackpot": 0,
|
||||
"max challenge": 5,
|
||||
"max difficulty": 0,
|
||||
"max level": 7,
|
||||
"max view": 2,
|
||||
@ -175,82 +174,21 @@ nlohmann::json mock_scores_json = R"([
|
||||
])"_json;
|
||||
|
||||
/* Use temporary directory to create mock paths for mock save-data JSON */
|
||||
|
||||
std::shared_ptr<fs::path> mock_preferences_path {
|
||||
new fs::path { fs::temp_directory_path() / "cakefoot_mock_preferences.json" },
|
||||
[] (fs::path* ptr) { fs::remove(*ptr); delete ptr; }
|
||||
};
|
||||
|
||||
std::shared_ptr<fs::path> mock_stats_path {
|
||||
new fs::path { fs::temp_directory_path() / "cakefoot_mock_stats.json" },
|
||||
[] (fs::path* ptr) { fs::remove(*ptr); delete ptr; }
|
||||
};
|
||||
|
||||
std::shared_ptr<fs::path> mock_progress_path {
|
||||
new fs::path { fs::temp_directory_path() / "cakefoot_mock_progress.json" },
|
||||
[] (fs::path* ptr) { fs::remove(*ptr); delete ptr; }
|
||||
};
|
||||
|
||||
std::shared_ptr<fs::path> mock_deprecated_progress_path {
|
||||
new fs::path { fs::temp_directory_path() / "cakefoot_mock_deprecated_progress.json" },
|
||||
[] (fs::path* ptr) { fs::remove(*ptr); delete ptr; }
|
||||
};
|
||||
|
||||
std::shared_ptr<fs::path> mock_scores_path {
|
||||
new fs::path { fs::temp_directory_path() / "cakefoot_mock_scores.json" },
|
||||
[] (fs::path* ptr) { fs::remove(*ptr); delete ptr; }
|
||||
};
|
||||
std::shared_ptr<fs::path> mock_preferences_path { sb::test::temp_path("cakefoot_mock_preferences.json") };
|
||||
std::shared_ptr<fs::path> mock_stats_path { sb::test::temp_path("cakefoot_mock_stats.json") };
|
||||
std::shared_ptr<fs::path> mock_progress_path { sb::test::temp_path("cakefoot_mock_progress.json") };
|
||||
std::shared_ptr<fs::path> mock_deprecated_progress_path { sb::test::temp_path("cakefoot_mock_deprecated_progress.json") };
|
||||
std::shared_ptr<fs::path> mock_scores_path { sb::test::temp_path("cakefoot_mock_scores.json") };
|
||||
|
||||
/* Mock paths for non-existing files */
|
||||
|
||||
std::shared_ptr<fs::path> mock_empty_stats_path {
|
||||
new fs::path { fs::temp_directory_path() / "cakefoot_mock_empty_stats.json" },
|
||||
[] (fs::path* ptr) { if (fs::exists(*ptr)) { fs::remove(*ptr); } delete ptr; }
|
||||
};
|
||||
|
||||
std::shared_ptr<fs::path> mock_empty_progress_path {
|
||||
new fs::path { fs::temp_directory_path() / "cakefoot_mock_empty_progress.json" },
|
||||
[] (fs::path* ptr) { if (fs::exists(*ptr)) { fs::remove(*ptr); } delete ptr; }
|
||||
};
|
||||
|
||||
std::shared_ptr<fs::path> mock_empty_scores_path {
|
||||
new fs::path { fs::temp_directory_path() / "cakefoot_mock_empty_scores.json" },
|
||||
[] (fs::path* ptr) { if (fs::exists(*ptr)) { fs::remove(*ptr); } delete ptr; }
|
||||
};
|
||||
|
||||
std::shared_ptr<fs::path> mock_empty_preferences_path {
|
||||
new fs::path { fs::temp_directory_path() / "cakefoot_mock_empty_preferences.json" },
|
||||
[] (fs::path* ptr) { if (fs::exists(*ptr)) { fs::remove(*ptr); } delete ptr; }
|
||||
};
|
||||
std::shared_ptr<fs::path> mock_empty_stats_path { sb::test::temp_path("cakefoot_mock_empty_stats.json") };
|
||||
std::shared_ptr<fs::path> mock_empty_progress_path { sb::test::temp_path("cakefoot_mock_empty_progress.json") };
|
||||
std::shared_ptr<fs::path> mock_empty_scores_path { sb::test::temp_path("cakefoot_mock_empty_scores.json") };
|
||||
std::shared_ptr<fs::path> mock_empty_preferences_path { sb::test::temp_path("cakefoot_mock_empty_preferences.json") };
|
||||
|
||||
/* Mock paths for testing bank parses */
|
||||
|
||||
std::shared_ptr<fs::path> mock_zero_arcade_bank_path {
|
||||
new fs::path { fs::temp_directory_path() / "cakefoot_mock_zero_arcade_bank.json" },
|
||||
[] (fs::path* ptr) { if (fs::exists(*ptr)) { fs::remove(*ptr); } delete ptr; }
|
||||
};
|
||||
|
||||
std::shared_ptr<fs::path> mock_zero_quest_bank_path {
|
||||
new fs::path { fs::temp_directory_path() / "cakefoot_mock_zero_quest_bank.json" },
|
||||
[] (fs::path* ptr) { if (fs::exists(*ptr)) { fs::remove(*ptr); } delete ptr; }
|
||||
};
|
||||
|
||||
namespace
|
||||
{
|
||||
void _write_json(fs::path path, const nlohmann::json& json)
|
||||
{
|
||||
std::ofstream file {path};
|
||||
if (file << std::setw(4) << json << std::endl)
|
||||
{
|
||||
sb::Log::Multi() << "Successfully wrote JSON to " << path << sb::Log::end;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb::Log::Multi(sb::Log::ERR) << "Could not write JSON to " << path << sb::Log::end;
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
std::shared_ptr<fs::path> mock_zero_arcade_bank_path { sb::test::temp_path("cakefoot_mock_zero_arcade_bank.json") };
|
||||
std::shared_ptr<fs::path> mock_zero_quest_bank_path { sb::test::temp_path("cakefoot_mock_zero_quest_bank.json") };
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
@ -262,16 +200,16 @@ int main(int argc, char** argv)
|
||||
/* Rig the stats so the last time stamp was recent and the day will not match today */
|
||||
std::chrono::time_point<std::chrono::system_clock> now { std::chrono::system_clock::now() };
|
||||
mock_stats_json["timestamp"] = sb::time::epoch_minutes(now - std::chrono::hours(23));
|
||||
mock_stats_json["day"] = "(o. 3.o)";
|
||||
mock_stats_json["day"] = "ฅ^•ﻌ•^ฅ";
|
||||
|
||||
/* Write mock data to temporary files */
|
||||
_write_json(*mock_preferences_path, mock_preferences_json);
|
||||
_write_json(*mock_progress_path, mock_progress_json);
|
||||
_write_json(*mock_deprecated_progress_path, mock_deprecated_progress_json);
|
||||
_write_json(*mock_stats_path, mock_stats_json);
|
||||
_write_json(*mock_scores_path, mock_scores_json);
|
||||
_write_json(*mock_zero_arcade_bank_path, mock_zero_arcade_bank_json);
|
||||
_write_json(*mock_zero_quest_bank_path, mock_zero_quest_bank_json);
|
||||
sb::test::write_json(*mock_preferences_path, mock_preferences_json);
|
||||
sb::test::write_json(*mock_progress_path, mock_progress_json);
|
||||
sb::test::write_json(*mock_deprecated_progress_path, mock_deprecated_progress_json);
|
||||
sb::test::write_json(*mock_stats_path, mock_stats_json);
|
||||
sb::test::write_json(*mock_scores_path, mock_scores_json);
|
||||
sb::test::write_json(*mock_zero_arcade_bank_path, mock_zero_arcade_bank_json);
|
||||
sb::test::write_json(*mock_zero_quest_bank_path, mock_zero_quest_bank_json);
|
||||
|
||||
return session.run();
|
||||
}
|
||||
@ -294,6 +232,31 @@ private:
|
||||
{
|
||||
validate_date(time);
|
||||
}
|
||||
|
||||
const MultiChunk& mock_bgm()
|
||||
{
|
||||
return bgm;
|
||||
}
|
||||
|
||||
const std::map<std::string, sb::audio::Chunk>& mock_audio()
|
||||
{
|
||||
return audio;
|
||||
}
|
||||
|
||||
void mock_reconfig()
|
||||
{
|
||||
reconfig();
|
||||
}
|
||||
|
||||
float mock_credits()
|
||||
{
|
||||
return credits;
|
||||
}
|
||||
|
||||
bool mock_operator_menu_active()
|
||||
{
|
||||
return operator_menu_active;
|
||||
}
|
||||
};
|
||||
|
||||
public:
|
||||
@ -303,6 +266,7 @@ public:
|
||||
sb::progress::Achievements achievements;
|
||||
sb::progress::Progress stat_progress;
|
||||
sb::progress::Progress progress;
|
||||
sb::progress::Progress preferences;
|
||||
|
||||
MockCakefoot(
|
||||
fs::path progress, fs::path scores, fs::path preferences, fs::path stats, nlohmann::json merge = {},
|
||||
@ -334,15 +298,21 @@ public:
|
||||
{
|
||||
progress.load(cakefoot.configuration()("storage", "progress file").get<fs::path>());
|
||||
}
|
||||
if (fs::exists(cakefoot.configuration()("storage", "preferences file").get<fs::path>()))
|
||||
{
|
||||
preferences.load(cakefoot.configuration()("storage", "preferences file").get<fs::path>());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void run(MockCakefoot& mock, int count = 2)
|
||||
{
|
||||
bool started = false;
|
||||
bool checked_challenges = false;
|
||||
int resets = 0;
|
||||
int downs = 0;
|
||||
int toggles = 0;
|
||||
std::size_t toggles = 0;
|
||||
std::size_t toggle_target = 0;
|
||||
const nlohmann::json& challenge_list = mock.cakefoot.configuration()("challenge");
|
||||
mock.cakefoot.run([&](float timestamp){
|
||||
if (mock.cakefoot.configuration()("progress", "quest deaths") > 2)
|
||||
{
|
||||
@ -350,15 +320,102 @@ void run(MockCakefoot& mock, int count = 2)
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!started)
|
||||
mock.refresh_progress();
|
||||
std::size_t level = mock.cakefoot.configuration()("progress", "current level");
|
||||
const int& progress_challenge = mock.cakefoot.configuration()("progress", "current challenge").
|
||||
get_ref<const nlohmann::json::number_integer_t&>();
|
||||
if (!checked_challenges)
|
||||
{
|
||||
sb::Delegate::post("any");
|
||||
started = true;
|
||||
if (downs++ == 0)
|
||||
{
|
||||
sb::Delegate::post("down");
|
||||
sb::Delegate::post("right");
|
||||
if (challenge_list.at(progress_challenge).at("name") == "RESUME QUEST")
|
||||
{
|
||||
toggle_target = challenge_list.size() - 1;
|
||||
}
|
||||
else if (challenge_list.at(progress_challenge).at("name") == "NEW QUEST")
|
||||
{
|
||||
toggle_target = challenge_list.size() - 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
CAPTURE(challenge_list.at(progress_challenge).at("name"));
|
||||
FAIL("Unsupported initial test conditions or menu code has failed");
|
||||
}
|
||||
|
||||
/* Try posting the operator event to make sure it doesn't do anything. */
|
||||
sb::Delegate::post("operator");
|
||||
}
|
||||
else if (toggles == 0)
|
||||
{
|
||||
if (challenge_list.at(progress_challenge).at("name") == "ARCADE")
|
||||
{
|
||||
toggles++;
|
||||
}
|
||||
sb::Delegate::post("any");
|
||||
|
||||
/* Make sure operator mode wasn't triggered */
|
||||
CHECK_FALSE(mock.cakefoot.mock_operator_menu_active());
|
||||
}
|
||||
else if (toggles < toggle_target)
|
||||
{
|
||||
if (toggles == 2 || toggles == 3)
|
||||
{
|
||||
/* This is expected to not move the cursor to make sure the start button is disabled */
|
||||
sb::Delegate::post("up");
|
||||
}
|
||||
|
||||
if (challenge_list.at(progress_challenge).at("name") != "ARCADE")
|
||||
{
|
||||
toggles++;
|
||||
sb::Delegate::post("any");
|
||||
}
|
||||
}
|
||||
else if (toggles == toggle_target)
|
||||
{
|
||||
CHECK(challenge_list.at(progress_challenge).at("name") == "ARCADE");
|
||||
CHECK(mock.cakefoot.mock_audio().at("menu").playing());
|
||||
CHECK_FALSE(mock.cakefoot.mock_bgm().playing());
|
||||
sb::Delegate::post("any");
|
||||
sb::Delegate::post("any");
|
||||
sb::Delegate::post("any");
|
||||
sb::Delegate::post("any");
|
||||
toggles += 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (toggle_target == challenge_list.size() - 1)
|
||||
{
|
||||
CHECK(challenge_list.at(progress_challenge).at("name") == "RESUME QUEST");
|
||||
}
|
||||
else if (toggle_target == challenge_list.size() - 3)
|
||||
{
|
||||
CHECK(challenge_list.at(progress_challenge).at("name") == "NEW QUEST");
|
||||
}
|
||||
sb::Delegate::post("up");
|
||||
sb::Delegate::post("any");
|
||||
checked_challenges = true;
|
||||
toggles = 0;
|
||||
downs = 0;
|
||||
}
|
||||
}
|
||||
else if (mock.cakefoot.configuration()("progress", "max difficulty") < 1)
|
||||
{
|
||||
if (mock.cakefoot.configuration()("progress", "current level") < 22)
|
||||
if (level < mock.cakefoot.configuration()("levels").size() - 2)
|
||||
{
|
||||
const nlohmann::json& world { mock.cakefoot.configuration()("world") };
|
||||
for (std::size_t ii = 0; ii < world.size(); ii++)
|
||||
{
|
||||
if (ii == world.size() - 1 || level < world[ii + 1].at("start"))
|
||||
{
|
||||
CHECK(mock.cakefoot.mock_bgm().playing());
|
||||
CHECK(mock.cakefoot.mock_bgm().index() == ii);
|
||||
break;
|
||||
}
|
||||
}
|
||||
CHECK(challenge_list.at(progress_challenge).at("name") == "RESUME QUEST");
|
||||
CHECK(mock.cakefoot.configuration()("progress", "quest level") == level);
|
||||
sb::Delegate::post("skip forward");
|
||||
}
|
||||
else
|
||||
@ -366,33 +423,49 @@ void run(MockCakefoot& mock, int count = 2)
|
||||
sb::Delegate::post("any");
|
||||
}
|
||||
}
|
||||
else if (resets++ < 1)
|
||||
else if (resets < 1)
|
||||
{
|
||||
if (count > 1)
|
||||
{
|
||||
sb::Delegate::post("reset");
|
||||
}
|
||||
else
|
||||
if (count < 2)
|
||||
{
|
||||
mock.cakefoot.flag_to_end();
|
||||
}
|
||||
else
|
||||
{
|
||||
sb::Delegate::post("reset");
|
||||
resets++;
|
||||
}
|
||||
}
|
||||
else if (downs++ < 1)
|
||||
else if (downs++ == 0)
|
||||
{
|
||||
CHECK(challenge_list.at(progress_challenge).at("name") == "NEW QUEST");
|
||||
sb::Delegate::post("down");
|
||||
sb::Delegate::post("right");
|
||||
}
|
||||
else if (toggles++ < 2)
|
||||
{
|
||||
sb::Delegate::post("any");
|
||||
}
|
||||
else if (mock.cakefoot.configuration()("progress", "arcade level") < 22)
|
||||
{
|
||||
sb::Delegate::post("skip forward");
|
||||
if (toggles == 2)
|
||||
{
|
||||
sb::Delegate::post("up");
|
||||
sb::Delegate::post("any");
|
||||
}
|
||||
}
|
||||
else if (!mock.stat_progress.achievement_unlocked(mock.achievements["ACH_HOTCAKES"]))
|
||||
{
|
||||
sb::Delegate::post("any");
|
||||
mock.refresh_progress();
|
||||
if (mock.cakefoot.configuration()("progress", "arcade level") <
|
||||
mock.cakefoot.configuration()("levels").size() - 2)
|
||||
{
|
||||
if (level > 0)
|
||||
{
|
||||
CHECK(challenge_list.at(progress_challenge).at("name") == "RESUME ARCADE");
|
||||
CHECK(mock.cakefoot.configuration()("progress", "arcade level") == level);
|
||||
}
|
||||
sb::Delegate::post("skip forward");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb::Delegate::post("any");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -503,6 +576,7 @@ TEST_CASE("D'ohnut")
|
||||
|
||||
/* Mock the D'ohnut achievement so it happens at any point after the beginning of the line */
|
||||
mock.cakefoot.configuration()["achievements"][4]["goal"] = 0.01f;
|
||||
mock.cakefoot.mock_reconfig();
|
||||
|
||||
/* Run until three deaths */
|
||||
run(mock, 1);
|
||||
@ -568,7 +642,7 @@ TEST_CASE("In-progress")
|
||||
|
||||
/* Reset progress files, so they won't affect the next test */
|
||||
fs::remove(*mock_stats_path);
|
||||
_write_json(*mock_stats_path, mock_stats_json);
|
||||
sb::test::write_json(*mock_stats_path, mock_stats_json);
|
||||
fs::remove(*mock_empty_progress_path);
|
||||
}
|
||||
|
||||
@ -679,7 +753,7 @@ TEST_CASE("Existing stats")
|
||||
|
||||
/* Launch a game */
|
||||
mock.cakefoot.run([&](float timestamp){
|
||||
if (timestamp > 5.0f)
|
||||
if (timestamp > 10.0f)
|
||||
{
|
||||
mock.cakefoot.flag_to_end();
|
||||
}
|
||||
@ -698,6 +772,405 @@ TEST_CASE("Existing stats")
|
||||
mock.cakefoot.quit();
|
||||
}
|
||||
|
||||
TEST_CASE("Idle timeout")
|
||||
{
|
||||
/* Mock demo and arcade-only modes */
|
||||
for (const std::string mode : {"demo", "arcade"})
|
||||
{
|
||||
/* Fresh installation. */
|
||||
MockCakefoot mock {
|
||||
*mock_empty_progress_path,
|
||||
*mock_empty_scores_path,
|
||||
*mock_empty_preferences_path,
|
||||
*mock_empty_stats_path
|
||||
};
|
||||
|
||||
/* Mock a quick idle timeout. */
|
||||
mock.cakefoot.configuration()[mode]["idle timeout"] = 2.0f;
|
||||
mock.cakefoot.configuration()[mode]["countdown display timeout"] = 2.0f;
|
||||
if (mode == "demo")
|
||||
{
|
||||
mock.cakefoot.configuration()["demo"]["active"] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
mock.cakefoot.configuration()["arcade"]["arcade only"] = true;
|
||||
}
|
||||
|
||||
/* Launch */
|
||||
bool started = false;
|
||||
bool button_released = false;
|
||||
mock.cakefoot.run([&](float timestamp){
|
||||
/* Automatically fail if running for more than 15 seconds */
|
||||
if (timestamp > 15.0f)
|
||||
{
|
||||
CAPTURE(mode);
|
||||
FAIL("Idle timeout test failed");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!started)
|
||||
{
|
||||
/* Start game */
|
||||
sb::Delegate::post("any");
|
||||
started = true;
|
||||
}
|
||||
else if (!button_released)
|
||||
{
|
||||
/* Stop moving */
|
||||
sb::Delegate::post("any", true);
|
||||
button_released = true;
|
||||
}
|
||||
|
||||
/* Use the currently playing audio to determine if the main menu has been returned to, confirming idle
|
||||
* reset. */
|
||||
else if (mock.cakefoot.mock_audio().at("menu").playing() && !mock.cakefoot.mock_bgm().playing())
|
||||
{
|
||||
mock.cakefoot.flag_to_end();
|
||||
}
|
||||
mock.cakefoot.draw(timestamp);
|
||||
}
|
||||
});
|
||||
mock.cakefoot.quit();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Credits")
|
||||
{
|
||||
/* Test five different configurations for credits */
|
||||
for (const nlohmann::json& params : nlohmann::json {
|
||||
{ "four", true, 4.0f, 1.0f, 0.0f },
|
||||
{ "one", true, 1.0f, 1.0f, 0.0f },
|
||||
{ "double", true, 50.5f, 25.25f, 0.0f },
|
||||
{ "max", true, 2.0f, 1.0f, 3.0f },
|
||||
{ "none", false, 4.0f, 1.0f, 0.0f } })
|
||||
{
|
||||
/* Mock arcade cabinet version requiring credits to play (other than the last configuration). */
|
||||
MockCakefoot mock {
|
||||
*mock_empty_progress_path,
|
||||
*mock_empty_scores_path,
|
||||
*mock_empty_preferences_path,
|
||||
*mock_empty_stats_path,
|
||||
{{ "arcade",
|
||||
{
|
||||
{ "arcade only", true },
|
||||
{ "credits enabled", params[1] },
|
||||
{ "idle timeout", 2.0f },
|
||||
{ "countdown display timeout", 2.0f },
|
||||
{ "credits required", params[2] },
|
||||
{ "credit increase per event", params[3] },
|
||||
{ "max credits", params[4] }
|
||||
} } }
|
||||
};
|
||||
|
||||
/* Launch */
|
||||
bool running = false;
|
||||
std::optional<float> run_start;
|
||||
int try_count = 0;
|
||||
mock.cakefoot.run([&](float timestamp){
|
||||
/* Automatically fail if running for more than 10 seconds */
|
||||
if (running && timestamp - *run_start > 10.0f)
|
||||
{
|
||||
CAPTURE(params);
|
||||
FAIL("Credits test failed");
|
||||
}
|
||||
else if (!running)
|
||||
{
|
||||
running = true;
|
||||
run_start = timestamp;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (try_count == 0)
|
||||
{
|
||||
/* Always start by trying to start the game */
|
||||
sb::Delegate::post("any");
|
||||
sb::Delegate::post("any", true);
|
||||
try_count++;
|
||||
}
|
||||
else if (try_count == 1)
|
||||
{
|
||||
if (params[0] == "none")
|
||||
{
|
||||
/* If there was no credit requirement, the game should have begun. */
|
||||
CHECK(mock.cakefoot.mock_bgm().playing());
|
||||
mock.cakefoot.flag_to_end();
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Make sure the game didn't start */
|
||||
CHECK(mock.cakefoot.mock_audio().at("menu").playing());
|
||||
|
||||
/* Try to start game again, then add a credit. */
|
||||
sb::Delegate::post("any");
|
||||
sb::Delegate::post("any", true);
|
||||
sb::Delegate::post("add credit");
|
||||
}
|
||||
try_count++;
|
||||
}
|
||||
else if (try_count == 2)
|
||||
{
|
||||
/* Check the credit account */
|
||||
if (params[0] != "double")
|
||||
{
|
||||
CHECK(mock.cakefoot.mock_credits() == 1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK(mock.cakefoot.mock_credits() == 25.25f);
|
||||
}
|
||||
|
||||
/* Make sure the game didn't start */
|
||||
CHECK(mock.cakefoot.mock_audio().at("menu").playing());
|
||||
|
||||
/* Try to start game again, then add another credit. */
|
||||
sb::Delegate::post("any");
|
||||
sb::Delegate::post("any", true);
|
||||
sb::Delegate::post("add credit");
|
||||
try_count++;
|
||||
}
|
||||
else if (try_count == 3)
|
||||
{
|
||||
if (params[0] == "one")
|
||||
{
|
||||
/* Only one credit was required */
|
||||
CHECK(mock.cakefoot.mock_bgm().playing());
|
||||
mock.cakefoot.flag_to_end();
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Check the credit account */
|
||||
if (params[0] != "double")
|
||||
{
|
||||
CHECK(mock.cakefoot.mock_credits() == 2.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK(mock.cakefoot.mock_credits() == 50.5f);
|
||||
}
|
||||
|
||||
/* Make sure the game didn't start */
|
||||
CHECK(mock.cakefoot.mock_audio().at("menu").playing());
|
||||
|
||||
/* Try starting again */
|
||||
sb::Delegate::post("any");
|
||||
sb::Delegate::post("any", true);
|
||||
}
|
||||
try_count++;
|
||||
}
|
||||
else if (try_count == 4)
|
||||
{
|
||||
if (params[0] == "double" || params[0] == "max")
|
||||
{
|
||||
/* Credit requirement should be satisfied */
|
||||
CHECK(mock.cakefoot.mock_bgm().playing());
|
||||
|
||||
if (params[0] == "max")
|
||||
{
|
||||
/* Test the max credits feature */
|
||||
sb::Delegate::post("add credit");
|
||||
sb::Delegate::post("add credit");
|
||||
}
|
||||
else
|
||||
{
|
||||
/* End test */
|
||||
mock.cakefoot.flag_to_end();
|
||||
}
|
||||
}
|
||||
|
||||
/* Add two more credits for final test and try again */
|
||||
sb::Delegate::post("add credit");
|
||||
sb::Delegate::post("add credit");
|
||||
sb::Delegate::post("any");
|
||||
sb::Delegate::post("any", true);
|
||||
try_count++;
|
||||
}
|
||||
else if (try_count == 5)
|
||||
{
|
||||
if (params[0] == "max")
|
||||
{
|
||||
/* Make sure credits didn't go over the max */
|
||||
CHECK(mock.cakefoot.mock_credits() == 3.0f);
|
||||
mock.cakefoot.flag_to_end();
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Make sure credits were removed from account and game started. */
|
||||
CHECK(mock.cakefoot.mock_credits() == 0.0f);
|
||||
CHECK(mock.cakefoot.mock_bgm().playing());
|
||||
}
|
||||
try_count++;
|
||||
}
|
||||
else if (try_count == 6)
|
||||
{
|
||||
/* Wait for arcade run to reset, then try another run */
|
||||
if (mock.cakefoot.mock_audio().at("menu").playing())
|
||||
{
|
||||
sb::Delegate::post("any");
|
||||
sb::Delegate::post("any", true);
|
||||
try_count++;
|
||||
}
|
||||
}
|
||||
else if (try_count == 7)
|
||||
{
|
||||
/* Make sure the game didn't start */
|
||||
CHECK_FALSE(mock.cakefoot.mock_bgm().playing());
|
||||
mock.cakefoot.flag_to_end();
|
||||
}
|
||||
mock.cakefoot.draw(timestamp);
|
||||
}
|
||||
});
|
||||
|
||||
/* Make sure the amount of tries matches the params */
|
||||
if (params[0] == "four")
|
||||
{
|
||||
CHECK(try_count == 7);
|
||||
}
|
||||
else if (params[0] == "one")
|
||||
{
|
||||
CHECK(try_count == 4);
|
||||
}
|
||||
else if (params[0] == "double")
|
||||
{
|
||||
CHECK(try_count == 5);
|
||||
}
|
||||
else if (params[0] == "max")
|
||||
{
|
||||
CHECK(try_count == 6);
|
||||
}
|
||||
else if (params[0] == "none")
|
||||
{
|
||||
CHECK(try_count == 2);
|
||||
}
|
||||
|
||||
mock.cakefoot.quit();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Operator menu")
|
||||
{
|
||||
/* Mock arcade cabinet version requiring credits to play (other than the last configuration). */
|
||||
MockCakefoot mock {
|
||||
*mock_empty_progress_path,
|
||||
*mock_empty_scores_path,
|
||||
*mock_empty_preferences_path,
|
||||
*mock_empty_stats_path,
|
||||
{{ "operator",
|
||||
{
|
||||
{ "enabled", true }
|
||||
} },
|
||||
{ "arcade",
|
||||
{
|
||||
{ "arcade only", true },
|
||||
{ "credits enabled", false },
|
||||
{ "idle timeout", 2.0f },
|
||||
{ "countdown display timeout", 2.0f },
|
||||
{ "credits required", 1.0f },
|
||||
{ "credit increase per event", 1.0f },
|
||||
{ "max credits", 0.0f }
|
||||
} } }
|
||||
};
|
||||
|
||||
/* Launch */
|
||||
bool running = false;
|
||||
std::optional<float> run_start;
|
||||
int toggles = 0;
|
||||
int downs = 0;
|
||||
int ups = 0;
|
||||
mock.cakefoot.run([&](float timestamp){
|
||||
/* Automatically fail if running for more than 10 seconds */
|
||||
if (running && timestamp - *run_start > 10.0f)
|
||||
{
|
||||
FAIL("Operator menu test timed out");
|
||||
}
|
||||
else if (!running)
|
||||
{
|
||||
running = true;
|
||||
run_start = timestamp;
|
||||
}
|
||||
else if (toggles == 0)
|
||||
{
|
||||
if (!mock.cakefoot.mock_operator_menu_active())
|
||||
{
|
||||
/* Open the operator menu */
|
||||
sb::Delegate::post("operator");
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Toggle credits enabled on */
|
||||
sb::Delegate::post("any");
|
||||
toggles++;
|
||||
}
|
||||
}
|
||||
else if (downs == 0)
|
||||
{
|
||||
/* Setting should not have been applied */
|
||||
CHECK_FALSE(mock.cakefoot.configuration()("arcade", "credits enabled"));
|
||||
|
||||
/* Move down to exit */
|
||||
for (int count = 0; count < 5; count++) sb::Delegate::post("down");
|
||||
downs = 5;
|
||||
}
|
||||
else if (ups == 0)
|
||||
{
|
||||
/* Try exiting which should require a confirmation press */
|
||||
sb::Delegate::post("any");
|
||||
|
||||
/* Move up to apply & save */
|
||||
sb::Delegate::post("up");
|
||||
ups++;
|
||||
}
|
||||
else if (downs == 5)
|
||||
{
|
||||
/* Try moving to credit increase, deleting all characters, and changing value */
|
||||
sb::Delegate::post("up");
|
||||
sb::Delegate::post("up");
|
||||
sb::Delegate::post("any");
|
||||
sb::Delegate::post("up");
|
||||
sb::Delegate::post("up");
|
||||
sb::Delegate::post("any");
|
||||
sb::Delegate::post("any");
|
||||
sb::Delegate::post("any");
|
||||
sb::Delegate::post("any");
|
||||
sb::Delegate::post("up");
|
||||
sb::Delegate::post("up");
|
||||
sb::Delegate::post("any");
|
||||
sb::Delegate::post("down");
|
||||
sb::Delegate::post("down");
|
||||
sb::Delegate::post("down");
|
||||
sb::Delegate::post("any");
|
||||
ups += 2;
|
||||
|
||||
/* Try the apply and save button */
|
||||
sb::Delegate::post("down");
|
||||
sb::Delegate::post("down");
|
||||
sb::Delegate::post("any");
|
||||
|
||||
/* Move down to exit and press the button */
|
||||
sb::Delegate::post("down");
|
||||
sb::Delegate::post("any");
|
||||
downs += 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
mock.cakefoot.flag_to_end();
|
||||
}
|
||||
});
|
||||
|
||||
/* Check if the menu exited, all buttons were pressed, and the settings were applied */
|
||||
CHECK_FALSE(mock.cakefoot.mock_operator_menu_active());
|
||||
CHECK(toggles == 1);
|
||||
CHECK(ups == 3);
|
||||
CHECK(downs == 8);
|
||||
CHECK(mock.cakefoot.configuration()("arcade", "credits enabled"));
|
||||
CHECK(mock.cakefoot.configuration()("arcade", "credit increase per event") == 9.0f);
|
||||
|
||||
/* Quit the game and check if the setting was saved to file */
|
||||
mock.cakefoot.quit();
|
||||
mock.refresh_progress();
|
||||
CHECK(mock.preferences.read("config", "arcade", "credits enabled"));
|
||||
}
|
||||
|
||||
/* Test loading stats and achievements. Test transferring stats from progress to stats. Test auto unlocking
|
||||
* achievements from progress data. */
|
||||
|
||||
@ -706,3 +1179,5 @@ TEST_CASE("Existing stats")
|
||||
/* Test distance traveled stat increase */
|
||||
|
||||
/* Test timing of stat writing */
|
||||
|
||||
/* Test missing sound device */
|
||||
|
149
src/update_arcade_cabinet.py
Normal file
@ -0,0 +1,149 @@
|
||||
# This program is used to update a game on a dedicated arcade cabinet running Linux. The system must be specially
|
||||
# configured to be compatible with the program.
|
||||
#
|
||||
# The program downloads the latest archive of a distributable, unzips it, and creates a symlink to the unzipped archive.
|
||||
#
|
||||
# The repository at the given URL must contain a JSON file with "version", "path", and "sha1" fields.
|
||||
|
||||
import os
|
||||
import zipfile
|
||||
import tempfile
|
||||
import pathlib
|
||||
import hashlib
|
||||
import shutil
|
||||
import argparse
|
||||
import requests
|
||||
from requests.compat import urljoin
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
# Exit status stays 1 (failure) until download and installation are complete
|
||||
exit_status = 1
|
||||
|
||||
# Create a CLI
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Check the distributables folder on the remote for an info file describing the latest version. If "
|
||||
"the latest version is not equal to the version passed to the CLI, download and extract the latest "
|
||||
"version, and add or update a symlink.\n\n"
|
||||
"If authentication is required by the remote, credentials can be added to the ~/.netrc file."),
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument("version", help="The version currently installed")
|
||||
parser.add_argument("base", help="The base URL where the distributable is located")
|
||||
parser.add_argument("info", help="File name of the JSON info file")
|
||||
parser.add_argument(
|
||||
"symlink", type=pathlib.Path, help="Path to a symlink that should be set to link to the installation")
|
||||
parser.add_argument("timeout", type=int, help="Seconds to wait before failing the initial request")
|
||||
parser.add_argument(
|
||||
"directory", type=pathlib.Path,
|
||||
help="Directory containing the symlink. This will be the working directory of the script.")
|
||||
parser.add_argument(
|
||||
"--storage", help="Path to existing save data to be copied over", type=pathlib.Path, default="storage")
|
||||
arguments = parser.parse_args()
|
||||
|
||||
# Change to the working directory
|
||||
os.chdir(arguments.directory)
|
||||
|
||||
# Get the JSON info file containing the relative URL of the latest distributable. Use this to build the URL of
|
||||
# the file to download.
|
||||
response = requests.get(urljoin(arguments.base, arguments.info), timeout=arguments.timeout)
|
||||
|
||||
# Raise an exception if the request was unsuccessful. The program will end here unless the request was successful.
|
||||
response.raise_for_status()
|
||||
|
||||
# Parse the requested file as JSON. If the file cannot be parsed as JSON, an exception will be raised, and the
|
||||
# program will end.
|
||||
info = response.json()
|
||||
|
||||
# Make sure the info file contains all the necessary fields.
|
||||
if "version" not in info or "path" not in info or "sha1" not in info:
|
||||
print(f"Info file does not contain all necessary information: {info}")
|
||||
else:
|
||||
|
||||
# If remote and local versions match, print a message and end program.
|
||||
if info["version"] == arguments.version:
|
||||
print(f"Game is up to date - remote version {info['version']} matches local version {arguments.version}.")
|
||||
|
||||
# If versions don't match, that indicates the local version is not up to date with the latest on the remote.
|
||||
else:
|
||||
|
||||
# Request the latest distributable. Use streaming since the file is more than 20MB.
|
||||
url_latest = urljoin(arguments.base, info["path"])
|
||||
destination = pathlib.Path(info["path"])
|
||||
response = requests.get(url_latest, stream=True)
|
||||
|
||||
# Raise an exception if the request was unsuccessful. The program will end here unless the request was
|
||||
# successful.
|
||||
response.raise_for_status()
|
||||
|
||||
# Open a local file with the same name as the file on the remote. Stream the raw data using copyfileobj.
|
||||
with open(destination, 'wb') as output:
|
||||
shutil.copyfileobj(response.raw, output)
|
||||
|
||||
# Get the hash of the downloaded file
|
||||
sha1 = hashlib.sha1()
|
||||
with open(destination, 'rb') as downloaded:
|
||||
sha1.update(downloaded.read())
|
||||
|
||||
# If the hash doesn't match what the info file reported, fail the download and remove the corrupt file.
|
||||
if sha1.hexdigest() != info["sha1"]:
|
||||
print(f"Download failed because hash of remote file '{info['sha1']}' did not match hash of downloaded "
|
||||
f"data '{sha1.hexdigest()}'.")
|
||||
destination.unlink()
|
||||
|
||||
# Download was successful.
|
||||
else:
|
||||
print(f"Downloaded {url_latest} to {destination}")
|
||||
|
||||
# Unzip the download to a temporary location and get the name of the root folder in the archive so it
|
||||
# can be renamed to the installation directory. Make the game's binary executable.
|
||||
with zipfile.ZipFile(destination, 'r') as archive:
|
||||
root, = zipfile.Path(archive).iterdir()
|
||||
archive.extractall(tempfile.gettempdir())
|
||||
download_location = pathlib.Path(tempfile.gettempdir()) / root.name
|
||||
os.chmod(download_location / (str(arguments.symlink) + ".x64"), 0o755)
|
||||
destination.unlink()
|
||||
print(f"Unzipped {destination} to {download_location}")
|
||||
|
||||
# Make sure if there is existing data that it is a symbolic link, so that it can be safely removed
|
||||
# and re-linked.
|
||||
symlink = arguments.symlink
|
||||
if symlink.exists() and not symlink.is_symlink():
|
||||
print(f"Existing data at {symlink} is expected to be a symbolic link, but a regular file was "
|
||||
"detected.")
|
||||
else:
|
||||
|
||||
# Remove .zip from file name to get the installation directory name. Change the name of the folder
|
||||
# to the installation directory name, then use copytree to bring it to the current working
|
||||
# directory. Using copytree with dirs_exist_ok prevents existing data from breaking the
|
||||
# installation.
|
||||
installation = destination.stem
|
||||
temp_location = pathlib.Path(tempfile.gettempdir()) / installation
|
||||
shutil.move(download_location, temp_location)
|
||||
shutil.copytree(temp_location, installation, dirs_exist_ok=True)
|
||||
shutil.rmtree(temp_location)
|
||||
print(f"Moved download from {temp_location} to {installation}")
|
||||
|
||||
if symlink.exists():
|
||||
|
||||
# Copy existing save data to new distributable
|
||||
storage_path = symlink / arguments.storage
|
||||
new_storage_path = installation / arguments.storage
|
||||
if storage_path.exists() and storage_path.resolve() != new_storage_path.resolve():
|
||||
shutil.copytree(storage_path, new_storage_path, dirs_exist_ok=True)
|
||||
print(f"Copied save data from {storage_path} to {new_storage_path}")
|
||||
|
||||
# Remove current symlink
|
||||
symlink.unlink()
|
||||
print(f"Removed old symlink {symlink}")
|
||||
|
||||
# Create symlink to download
|
||||
symlink.symlink_to(installation, target_is_directory=True)
|
||||
print(f"Linked {symlink} to {installation}")
|
||||
|
||||
# Installation was successful
|
||||
exit_status = 0
|
||||
|
||||
# This will only be 0 (success) if the full installation ran
|
||||
sys.exit(exit_status)
|