add Dockerfile

master
greg 1 year ago
commit 09d0d54d61
  1. 60
      Dockerfile
  2. 185
      Makefile
  3. 158
      README.md
  4. 299
      src/input.c
  5. 191
      src/input.h
  6. 4274
      src/libs/pl_mpeg.h
  7. 728
      src/libs/qoa.h
  8. 11057
      src/libs/sokol_app.h
  9. 2596
      src/libs/sokol_audio.h
  10. 324
      src/libs/sokol_time.h
  11. 1724
      src/libs/stb_image_write.h
  12. 81
      src/mem.c
  13. 17
      src/mem.h
  14. 16
      src/platform.h
  15. 350
      src/platform_sdl.c
  16. 256
      src/platform_sokol.c
  17. 64
      src/render.h
  18. 1001
      src/render_gl.c
  19. 369
      src/render_software.c
  20. 81
      src/system.c
  21. 23
      src/system.h
  22. 116
      src/types.c
  23. 184
      src/types.h
  24. 68
      src/utils.c
  25. 139
      src/utils.h
  26. 238
      src/wasm-index.html
  27. 167
      src/wipeout/camera.c
  28. 32
      src/wipeout/camera.h
  29. 260
      src/wipeout/droid.c
  30. 39
      src/wipeout/droid.h
  31. 637
      src/wipeout/game.c
  32. 270
      src/wipeout/game.h
  33. 244
      src/wipeout/hud.c
  34. 9
      src/wipeout/hud.h
  35. 320
      src/wipeout/image.c
  36. 34
      src/wipeout/image.h
  37. 486
      src/wipeout/ingame_menus.c
  38. 13
      src/wipeout/ingame_menus.h
  39. 105
      src/wipeout/intro.c
  40. 8
      src/wipeout/intro.h
  41. 541
      src/wipeout/main_menu.c
  42. 7
      src/wipeout/main_menu.h
  43. 245
      src/wipeout/menu.c
  44. 65
      src/wipeout/menu.h
  45. 778
      src/wipeout/object.c
  46. 383
      src/wipeout/object.h
  47. 70
      src/wipeout/particle.c
  48. 32
      src/wipeout/particle.h
  49. 279
      src/wipeout/race.c
  50. 14
      src/wipeout/race.h
  51. 261
      src/wipeout/scene.c
  52. 15
      src/wipeout/scene.h
  53. 393
      src/wipeout/sfx.c
  54. 85
      src/wipeout/sfx.h
  55. 1006
      src/wipeout/ship.c
  56. 168
      src/wipeout/ship.h
  57. 536
      src/wipeout/ship_ai.c
  58. 14
      src/wipeout/ship_ai.h
  59. 582
      src/wipeout/ship_player.c
  60. 20
      src/wipeout/ship_player.h
  61. 45
      src/wipeout/title.c
  62. 8
      src/wipeout/title.h
  63. 387
      src/wipeout/track.c
  64. 104
      src/wipeout/track.h
  65. 213
      src/wipeout/ui.c
  66. 56
      src/wipeout/ui.h
  67. 689
      src/wipeout/weapon.c
  68. 51
      src/wipeout/weapon.h
  69. 2
      wipeout/game_data_goes_here.txt

@ -0,0 +1,60 @@
# wipeout wasm in docker
FROM debian:bullseye
LABEL Greg Lebreton <greg.lebreton@hotmail.com
ENV ROOT_WWW_PATH /var/www/html
RUN apt-get update && apt-get install -y \
git \
make \
libx11-dev \
libxcursor-dev \
libxi-dev \
libasound2-dev \
ca-certificates \
unzip \
sed \
p7zip-full \
emscripten \
coffeescript \
xz-utils \
nginx \
wget \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# sinon le build failed
RUN emcc --version
# clone repo git
RUN cd / \
&& git clone https://github.com/phoboslab/wipeout-rewrite.git \
&& cd wipeout-rewrite
# copie des fichiers necessaires (wipeout-datas)
# COPY ./wipeout /wipeout-rewrite
# wipeout datas
RUN cd /tmp \
&& wget https://phoboslab.org/files/wipeout-data-v01.zip \
&& unzip wipeout-data-v01.zip -d wipeout \
&& cp -r wipeout/* /wipeout-rewrite
# build
RUN cd wipeout-rewrite \
&& make wasm
# mise en place des fichiers
RUN cp /wipeout-rewrite/build/wasm/* /var/www/html \
&& cp /wipeout-rewrite/src/wasm-index.html /var/www/html/index.html \
&& cp -r /wipeout-rewrite/wipeout /var/www/html
WORKDIR ${ROOT_WWW_PATH}
EXPOSE 80
COPY entrypoint.sh /
CMD [ "sh", "/entrypoint.sh"]

@ -0,0 +1,185 @@
CC ?= gcc
EMCC ?= emcc
UNAME_S := $(shell uname -s)
RENDERER ?= GL
USE_GLX ?= false
DEBUG ?= false
L_FLAGS ?= -lm -rdynamic
C_FLAGS ?= -std=gnu99 -Wall -Wno-unused-variable
ifeq ($(DEBUG), true)
C_FLAGS := $(C_FLAGS) -g
else
C_FLAGS := $(C_FLAGS) -O3
endif
# Rendeder ---------------------------------------------------------------------
ifeq ($(RENDERER), GL)
RENDERER_SRC = src/render_gl.c
C_FLAGS := $(C_FLAGS) -DRENDERER_GL
else ifeq ($(RENDERER), SOFTWARE)
RENDERER_SRC = src/render_software.c
C_FLAGS := $(C_FLAGS) -DRENDERER_SOFTWARE
else
$(error Unknown RENDERER)
endif
ifeq ($(GL_VERSION), GLES2)
C_FLAGS := $(C_FLAGS) -DUSE_GLES2
endif
# macOS ------------------------------------------------------------------------
ifeq ($(UNAME_S), Darwin)
C_FLAGS := $(C_FLAGS) -x objective-c -I/opt/homebrew/include -D_THREAD_SAFE -w
L_FLAGS := $(L_FLAGS) -L/opt/homebrew/lib -framework Foundation
ifeq ($(RENDERER), GL)
L_FLAGS := $(L_FLAGS) -lGLEW -GLU -framework OpenGL
endif
L_FLAGS_SDL = -lSDL2
L_FLAGS_SOKOL = -framework Cocoa -framework QuartzCore -framework AudioToolbox
# Linux ------------------------------------------------------------------------
else ifeq ($(UNAME_S), Linux)
ifeq ($(RENDERER), GL)
L_FLAGS := $(L_FLAGS) -lGLEW
# Prefer modern GLVND instead of legacy X11-only GLX
ifeq ($(USE_GLX), true)
L_FLAGS := $(L_FLAGS) -lGL
else
L_FLAGS := $(L_FLAGS) -lOpenGL
endif
endif
L_FLAGS_SDL = -lSDL2
L_FLAGS_SOKOL = -lX11 -lXcursor -pthread -lXi -ldl -lasound
# Windows ----------------------------------------------------------------------
else ifeq ($(OS), Windows_NT)
$(error TODO: FLAGS for windows have not been set up. Please modify this makefile and send a PR!)
else
$(error Unknown environment)
endif
# Source files -----------------------------------------------------------------
TARGET_NATIVE ?= wipegame
BUILD_DIR = build/obj/native
BUILD_DIR_WASM = build/obj/wasm
WASM_RELEASE_DIR ?= build/wasm
TARGET_WASM ?= $(WASM_RELEASE_DIR)/wipeout.js
TARGET_WASM_MINIMAL ?= $(WASM_RELEASE_DIR)/wipeout-minimal.js
COMMON_SRC = \
src/wipeout/race.c \
src/wipeout/camera.c \
src/wipeout/object.c \
src/wipeout/droid.c \
src/wipeout/ui.c \
src/wipeout/hud.c \
src/wipeout/image.c \
src/wipeout/game.c \
src/wipeout/menu.c \
src/wipeout/main_menu.c \
src/wipeout/ingame_menus.c \
src/wipeout/title.c \
src/wipeout/intro.c \
src/wipeout/scene.c \
src/wipeout/ship.c \
src/wipeout/ship_ai.c \
src/wipeout/ship_player.c \
src/wipeout/track.c \
src/wipeout/weapon.c \
src/wipeout/particle.c \
src/wipeout/sfx.c \
src/utils.c \
src/types.c \
src/system.c \
src/mem.c \
src/input.c \
$(RENDERER_SRC)
# Targets native ---------------------------------------------------------------
COMMON_OBJ = $(patsubst %.c, $(BUILD_DIR)/%.o, $(COMMON_SRC))
COMMON_DEPS = $(patsubst %.c, $(BUILD_DIR)/%.d, $(COMMON_SRC))
sdl: $(BUILD_DIR)/src/platform_sdl.o
sdl: $(COMMON_OBJ)
$(CC) $^ -o $(TARGET_NATIVE) $(L_FLAGS) $(L_FLAGS_SDL)
sokol: $(BUILD_DIR)/src/platform_sokol.o
sokol: $(COMMON_OBJ)
$(CC) $^ -o $(TARGET_NATIVE) $(L_FLAGS) $(L_FLAGS_SOKOL)
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
$(BUILD_DIR)/%.o: %.c
mkdir -p $(dir $@)
$(CC) $(C_FLAGS) -MMD -MP -c $< -o $@
-include $(COMMON_DEPS)
# Targets wasm -----------------------------------------------------------------
COMMON_OBJ_WASM = $(patsubst %.c, $(BUILD_DIR_WASM)/%.o, $(COMMON_SRC))
COMMON_DEPS_WASM = $(patsubst %.c, $(BUILD_DIR_WASM)/%.d, $(COMMON_SRC))
wasm: wasm_full wasm_minimal
cp src/wasm-index.html $(WASM_RELEASE_DIR)/game.html
wasm_full: $(BUILD_DIR_WASM)/src/platform_sokol.o
wasm_full: $(COMMON_OBJ_WASM)
mkdir -p $(WASM_RELEASE_DIR)
$(EMCC) $^ -o $(TARGET_WASM) -lGLEW -lGL \
-s ALLOW_MEMORY_GROWTH=1 \
-s ENVIRONMENT=web \
--preload-file wipeout
wasm_minimal: $(BUILD_DIR_WASM)/src/platform_sokol.o
wasm_minimal: $(COMMON_OBJ_WASM)
mkdir -p $(WASM_RELEASE_DIR)
$(EMCC) $^ -o $(TARGET_WASM_MINIMAL) -lGLEW -lGL \
-s ALLOW_MEMORY_GROWTH=1 \
-s ENVIRONMENT=web \
--preload-file wipeout \
--exclude-file wipeout/music \
--exclude-file wipeout/intro.mpeg
$(BUILD_DIR_WASM):
mkdir -p $(BUILD_DIR_WASM)
$(BUILD_DIR_WASM)/%.o: %.c
mkdir -p $(dir $@)
$(EMCC) $(C_FLAGS) -MMD -MP -c $< -o $@
-include $(COMMON_DEPS_WASM)
.PHONY: clean
clean:
$(RM) -rf $(BUILD_DIR) $(BUILD_DIR_WASM) $(WASM_RELEASE_DIR)

@ -0,0 +1,158 @@
# wipEout Rewrite
This is a re-implementation of the 1995 PSX game wipEout.
Play here: https://phoboslab.org/wipegame/
More info in my blog: https://phoboslab.org/log/2023/08/rewriting-wipeout
Work in progress. Expect bugs.
## Building
The game currently supports two different platform-backends: [SDL2](https://github.com/libsdl-org/SDL) and [Sokol](https://github.com/floooh/sokol). The only difference in features is that the SDL2 backend supports game controllers (joysticks, gamepads), while the Sokol backend does not.
### Linux
#### Ubuntu
```
# for SDL2 backend
apt install libsdl2-dev libglew-dev
make sdl
```
```
# for Sokol backend
apt install libx11-dev libxcursor-dev libxi-dev libasound2-dev
make sokol
```
#### Fedora
```
# for SDL2 backend
dnf install SDL2-devel glew-devel
make sdl
```
```
# for Sokol backend
dnf install libX11-devel libXi-devel alsa-lib-devel glew-devel libXcursor-devel
make sokol
```
### macOS
Currently only the SDL2 backend works. macOS is very picky about the GLSL shader version when compiling with Sokol and OpenGL3.3; it shouldn't be too difficult to get it working, but will probably require a bunch of `#ifdefs` for SDL and WASM. PRs welcome!
```
brew install sdl2 glew
make sdl
```
### Windows
In theory both backends should work on Windows, but the Makefile is missing the proper compiler flags. Please send a PR!
_todo_
### WASM
Install [emscripten](https://emscripten.org/) and activate emsdk, so that `emcc` is in your `PATH`. The WASM version automatically
selects the Sokol backend. I'm not sure what needs to be done to make the SDL2 backend work with WASM.
```
make wasm
```
This builds the minimal version (no music, no intro) as well as the full version.
### Flags
The makefile accepts several flags. You can specify them with `make FLAG=VALUE`
- `DEBUG``true` or `fals`, default is `false`. Whether to include debug symbols in the build.
- `RENDERER``GL` or `SOFTWARE`, default is `GL` (the `SOFTWARE` renderer is very much unfinished and only works with SDL)
- `USE_GLX``true` or `false`, default is `false` and uses `GLVND` over `GLX`. Only used for the linux build.
## Running
This repository does not contain the assets (textures, 3d models etc.) required to run the game. This code mostly assumes to have the PSX NTSC data, but some menu models from the PC version are loaded as well. Both of these can be easily found on archive.org and similar sites. The music (optional) needs to be provided in [QOA format](https://github.com/phoboslab/qoa). The intro video as MPEG1.
The directory structure is assumed to be as follows
```
./wipegame # the executable
./wipeout/textures/
./wipeout/music/track01.qoa
./wipeout/music/track02.qoa
...
```
Note that the blog post announcing this project may or may not provide a link to a ZIP containing all files needed. Who knows!
## Docker
```
docker build -t wipeout:1.0 .
docker run -p 80:80 wipeout:1.0
```
> http://localhost
## Ideas for improvements
PRs Welcome.
### Not yet implemented
Some things from the original game are not yet implemented in this rewrite. This includes
- screen shake effect
- game-end animations, formerly `Spline.cpp` (the end messages are just shown over the attract mode cameras)
- viewing highscores in options menu
- controller options menu
- reverb for sfx and music when there's more than 4 track faces (tunnels and such)
- some more? grep the source for `TODO` and `FIXME`
### Gameplay, Visuals
- less punishing physics for ship vs. ship collisions
- less punishing physics for sideways ship vs. track collisions (i.e. wall grinding like in newer wipEouts)
- somehow resolve the issue of inevitably running into an enemy that you just shot
- add option to lessen the roll in the internal view
- add additional external view that behaves more like in modern racing games
- dynamic lighting on ships
- allow lower resolutions and a drawing mode that resembles the PSX original
- the scene geometry could use some touch-ups to make an infinite draw distance option less awkward
- increase FOV when going over a boost
- better menu models for game exit and video options
- gamepad analog input feels like balancing an egg
- fix collision issues on junctions (also present in the original)
### Technical
- implement frustum culling for scene geometry, the track and ships. Currently everything within the fadeout radius is drawn.
- put all static geometry into a GPU-side buffer. Currently all triangles are constructed at draw time. Uploading geometry is complicated a bit by the fact that some scene animations and the ship's exhaust need to update geometry for each frame.
- the menu system is... not great. It's better than the 5000 lines of spaghetti that it was before, but the different layouts need a lot of `if`s
- the save data is just dumping the whole struct on disk. A textual format would be preferable.
- since this whole thing is relying on some custom assembled assets anyway, maybe all SFX should be in QOA format too (like the music). Or switch everything to Vorbis.
- a lot of functions assume that there's just one player. This needs to be fixed for a potential splitscreen mode.
## License
There is none. This code may or may not be based on the source code of the PC (ATI-Rage) version that was leaked in 2022. If it were, it would probably violate copyright law, but it may also fall under fair use ¯\\\_(ツ)\_/¯
Working with this source code is probably fine, considering that this game was originally released 28 years ago (in 1995), that the current copyright holders historically didn't care about any wipEout related files or code being available on the net and that the game is currently not purchasable in any shape or form.
In any case, you may NOT use this source code in a commercial release. A commercial release includes hosting it on a website that shows any forms of advertising.
PS.: Hey Sony! If you're reading this, I would love to work on a proper, officially sanctioned remaster. Please get in touch <3

@ -0,0 +1,299 @@
#include <string.h>
#include "input.h"
#include "utils.h"
static const char *button_names[] = {
NULL,
NULL,
NULL,
NULL,
[INPUT_KEY_A] = "A",
[INPUT_KEY_B] = "B",
[INPUT_KEY_C] = "C",
[INPUT_KEY_D] = "D",
[INPUT_KEY_E] = "E",
[INPUT_KEY_F] = "F",
[INPUT_KEY_G] = "G",
[INPUT_KEY_H] = "H",
[INPUT_KEY_I] = "I",
[INPUT_KEY_J] = "J",
[INPUT_KEY_K] = "K",
[INPUT_KEY_L] = "L",
[INPUT_KEY_M] = "M",
[INPUT_KEY_N] = "N",
[INPUT_KEY_O] = "O",
[INPUT_KEY_P] = "P",
[INPUT_KEY_Q] = "Q",
[INPUT_KEY_R] = "R",
[INPUT_KEY_S] = "S",
[INPUT_KEY_T] = "T",
[INPUT_KEY_U] = "U",
[INPUT_KEY_V] = "V",
[INPUT_KEY_W] = "W",
[INPUT_KEY_X] = "X",
[INPUT_KEY_Y] = "Y",
[INPUT_KEY_Z] = "Z",
[INPUT_KEY_1] = "1",
[INPUT_KEY_2] = "2",
[INPUT_KEY_3] = "3",
[INPUT_KEY_4] = "4",
[INPUT_KEY_5] = "5",
[INPUT_KEY_6] = "6",
[INPUT_KEY_7] = "7",
[INPUT_KEY_8] = "8",
[INPUT_KEY_9] = "9",
[INPUT_KEY_0] = "0",
[INPUT_KEY_RETURN] = "RETURN",
[INPUT_KEY_ESCAPE] = "ESCAPE",
[INPUT_KEY_BACKSPACE] = "BACKSP",
[INPUT_KEY_TAB] = "TAB",
[INPUT_KEY_SPACE] = "SPACE",
[INPUT_KEY_MINUS] = "MINUS",
[INPUT_KEY_EQUALS] = "EQUALS",
[INPUT_KEY_LEFTBRACKET] = "LBRACKET",
[INPUT_KEY_RIGHTBRACKET] = "RBRACKET",
[INPUT_KEY_BACKSLASH] = "BSLASH",
[INPUT_KEY_HASH] = "HASH",
[INPUT_KEY_SEMICOLON] = "SMICOL",
[INPUT_KEY_APOSTROPHE] = "APO",
[INPUT_KEY_TILDE] = "TILDE",
[INPUT_KEY_COMMA] = "COMMA",
[INPUT_KEY_PERIOD] = "PERIOD",
[INPUT_KEY_SLASH] = "SLASH",
[INPUT_KEY_CAPSLOCK] = "CAPS",
[INPUT_KEY_F1] = "F1",
[INPUT_KEY_F2] = "F2",
[INPUT_KEY_F3] = "F3",
[INPUT_KEY_F4] = "F4",
[INPUT_KEY_F5] = "F5",
[INPUT_KEY_F6] = "F6",
[INPUT_KEY_F7] = "F7",
[INPUT_KEY_F8] = "F8",
[INPUT_KEY_F9] = "F9",
[INPUT_KEY_F10] = "F10",
[INPUT_KEY_F11] = "F11",
[INPUT_KEY_F12] = "F12",
[INPUT_KEY_PRINTSCREEN] = "PRTSC",
[INPUT_KEY_SCROLLLOCK] = "SCRLK",
[INPUT_KEY_PAUSE] = "PAUSE",
[INPUT_KEY_INSERT] = "INSERT",
[INPUT_KEY_HOME] = "HOME",
[INPUT_KEY_PAGEUP] = "PG UP",
[INPUT_KEY_DELETE] = "DELETE",
[INPUT_KEY_END] = "END",
[INPUT_KEY_PAGEDOWN] = "PG DOWN",
[INPUT_KEY_RIGHT] = "RIGHT",
[INPUT_KEY_LEFT] = "LEFT",
[INPUT_KEY_DOWN] = "DOWN",
[INPUT_KEY_UP] = "UP",
[INPUT_KEY_NUMLOCK] = "NLOCK",
[INPUT_KEY_KP_DIVIDE] = "KPDIV",
[INPUT_KEY_KP_MULTIPLY] = "KPMUL",
[INPUT_KEY_KP_MINUS] = "KPMINUS",
[INPUT_KEY_KP_PLUS] = "KPPLUS",
[INPUT_KEY_KP_ENTER] = "KPENTER",
[INPUT_KEY_KP_1] = "KP1",
[INPUT_KEY_KP_2] = "KP2",
[INPUT_KEY_KP_3] = "KP3",
[INPUT_KEY_KP_4] = "KP4",
[INPUT_KEY_KP_5] = "KP5",
[INPUT_KEY_KP_6] = "KP6",
[INPUT_KEY_KP_7] = "KP7",
[INPUT_KEY_KP_8] = "KP8",
[INPUT_KEY_KP_9] = "KP9",
[INPUT_KEY_KP_0] = "KP0",
[INPUT_KEY_KP_PERIOD] = "KPPERIOD",
[INPUT_KEY_LCTRL] = "LCTRL",
[INPUT_KEY_LSHIFT] = "LSHIFT",
[INPUT_KEY_LALT] = "LALT",
[INPUT_KEY_LGUI] = "LGUI",
[INPUT_KEY_RCTRL] = "RCTRL",
[INPUT_KEY_RSHIFT] = "RSHIFT",
[INPUT_KEY_RALT] = "RALT",
NULL,
[INPUT_GAMEPAD_A] = "A",
[INPUT_GAMEPAD_Y] = "Y",
[INPUT_GAMEPAD_B] = "B",
[INPUT_GAMEPAD_X] = "X",
[INPUT_GAMEPAD_L_SHOULDER] = "LSHLDR",
[INPUT_GAMEPAD_R_SHOULDER] = "RSHLDR",
[INPUT_GAMEPAD_L_TRIGGER] = "LTRIG",
[INPUT_GAMEPAD_R_TRIGGER] = "RTRIG",
[INPUT_GAMEPAD_SELECT] = "SELECT",
[INPUT_GAMEPAD_START] = "START",
[INPUT_GAMEPAD_L_STICK_PRESS] = "LSTK",
[INPUT_GAMEPAD_R_STICK_PRESS] = "RSTK",
[INPUT_GAMEPAD_DPAD_UP] = "DPUP",
[INPUT_GAMEPAD_DPAD_DOWN] = "DPDOWN",
[INPUT_GAMEPAD_DPAD_LEFT] = "DPLEFT",
[INPUT_GAMEPAD_DPAD_RIGHT] = "DPRIGHT",
[INPUT_GAMEPAD_HOME] = "HOME",
[INPUT_GAMEPAD_L_STICK_UP] = "LSTKUP",
[INPUT_GAMEPAD_L_STICK_DOWN] = "LSTKDOWN",
[INPUT_GAMEPAD_L_STICK_LEFT] = "LSTKLEFT",
[INPUT_GAMEPAD_L_STICK_RIGHT] = "LSTKRIGHT",
[INPUT_GAMEPAD_R_STICK_UP] = "RSTKUP",
[INPUT_GAMEPAD_R_STICK_DOWN] = "RSTKDOWN",
[INPUT_GAMEPAD_R_STICK_LEFT] = "RSTKLEFT",
[INPUT_GAMEPAD_R_STICK_RIGHT] = "RSTKRIGHT",
NULL,
[INPUT_MOUSE_LEFT] = "MLEFT",
[INPUT_MOUSE_MIDDLE] = "MMIDDLE",
[INPUT_MOUSE_RIGHT] = "MRIGHT",
[INPUT_MOUSE_WHEEL_UP] = "MWUP",
[INPUT_MOUSE_WHEEL_DOWN] = "MWDOWN",
};
static float actions_state[INPUT_ACTION_MAX];
static bool actions_pressed[INPUT_ACTION_MAX];
static bool actions_released[INPUT_ACTION_MAX];
static uint8_t expected_button[INPUT_ACTION_MAX];
static uint8_t bindings[INPUT_LAYER_MAX][INPUT_BUTTON_MAX];
static input_capture_callback_t capture_callback;
static void *capture_user;
static int32_t mouse_x;
static int32_t mouse_y;
void input_init() {
input_unbind_all(INPUT_LAYER_SYSTEM);
input_unbind_all(INPUT_LAYER_USER);
}
void input_cleanup() {
}
void input_clear() {
clear(actions_pressed);
clear(actions_released);
}
void input_set_layer_button_state(input_layer_t layer, button_t button, float state) {
error_if(layer < 0 || layer >= INPUT_LAYER_MAX, "Invalid input layer %d", layer);
uint8_t action = bindings[layer][button];
if (action == INPUT_ACTION_NONE) {
return;
}
uint8_t expected = expected_button[action];
if (!expected || expected == button) {
state = (state > INPUT_DEADZONE) ? state : 0;
if (state && !actions_state[action]) {
actions_pressed[action] = true;
expected_button[action] = button;
}
else if (!state && actions_state[action]) {
actions_released[action] = true;
expected_button[action] = INPUT_BUTTON_NONE;
}
actions_state[action] = state;
}
}
void input_set_button_state(button_t button, float state) {
error_if(button < 0 || button >= INPUT_BUTTON_MAX, "Invalid input button %d", button);
input_set_layer_button_state(INPUT_LAYER_SYSTEM, button, state);
input_set_layer_button_state(INPUT_LAYER_USER, button, state);
if (capture_callback) {
if (state > INPUT_DEADZONE_CAPTURE) {
capture_callback(capture_user, button, 0);
}
}
}
void input_set_mouse_pos(int32_t x, int32_t y) {
mouse_x = x;
mouse_y = y;
}
void input_capture(input_capture_callback_t cb, void *user) {
capture_callback = cb;
capture_user = user;
input_clear();
}
void input_textinput(int32_t ascii_char) {
if (capture_callback) {
capture_callback(capture_user, INPUT_INVALID, ascii_char);
}
}
void input_bind(input_layer_t layer, button_t button, uint8_t action) {
error_if(button < 0 || button >= INPUT_BUTTON_MAX, "Invalid input button %d", button);
error_if(action < 0 || action >= INPUT_ACTION_MAX, "Invalid input action %d", action);
error_if(layer < 0 || layer >= INPUT_LAYER_MAX, "Invalid input layer %d", layer);
actions_state[action] = 0;
bindings[layer][button] = action;
}
uint8_t input_bound_to_action(button_t button) {
error_if(button < 0 || button >= INPUT_BUTTON_MAX, "Invalid input button %d", button);
return bindings[INPUT_LAYER_USER][button];
}
void input_unbind(input_layer_t layer, button_t button) {
error_if(layer < 0 || layer >= INPUT_LAYER_MAX, "Invalid input layer %d", layer);
error_if(button < 0 || button >= INPUT_BUTTON_MAX, "Invalid input button %d", button);
bindings[layer][button] = INPUT_ACTION_NONE;
}
void input_unbind_all(input_layer_t layer) {
error_if(layer < 0 || layer >= INPUT_LAYER_MAX, "Invalid input layer %d", layer);
for (uint32_t button = 0; button < INPUT_BUTTON_MAX; button++) {
input_unbind(layer, button);
}
}
float input_state(uint8_t action) {
error_if(action < 0 || action >= INPUT_ACTION_MAX, "Invalid input action %d", action);
return actions_state[action];
}
bool input_pressed(uint8_t action) {
error_if(action < 0 || action >= INPUT_ACTION_MAX, "Invalid input action %d", action);
return actions_pressed[action];
}
bool input_released(uint8_t action) {
error_if(action < 0 || action >= INPUT_ACTION_MAX, "Invalid input action %d", action);
return actions_released[action];
}
vec2_t input_mouse_pos() {
return vec2(mouse_x, mouse_y);
}
button_t input_name_to_button(const char *name) {
for (int32_t i = 0; i < INPUT_BUTTON_MAX; i++) {
if (button_names[i] && strcmp(name, button_names[i]) == 0) {
return i;
}
}
return INPUT_INVALID;
}
const char *input_button_to_name(button_t button) {
if (
button < 0 || button >= INPUT_BUTTON_MAX ||
!button_names[button]
) {
return NULL;
}
return button_names[button];
}

@ -0,0 +1,191 @@
#ifndef INPUT_H
#define INPUT_H
#include "types.h"
typedef enum {
INPUT_INVALID = 0,
INPUT_KEY_A = 4,
INPUT_KEY_B = 5,
INPUT_KEY_C = 6,
INPUT_KEY_D = 7,
INPUT_KEY_E = 8,
INPUT_KEY_F = 9,
INPUT_KEY_G = 10,
INPUT_KEY_H = 11,
INPUT_KEY_I = 12,
INPUT_KEY_J = 13,
INPUT_KEY_K = 14,
INPUT_KEY_L = 15,
INPUT_KEY_M = 16,
INPUT_KEY_N = 17,
INPUT_KEY_O = 18,
INPUT_KEY_P = 19,
INPUT_KEY_Q = 20,
INPUT_KEY_R = 21,
INPUT_KEY_S = 22,
INPUT_KEY_T = 23,
INPUT_KEY_U = 24,
INPUT_KEY_V = 25,
INPUT_KEY_W = 26,
INPUT_KEY_X = 27,
INPUT_KEY_Y = 28,
INPUT_KEY_Z = 29,
INPUT_KEY_1 = 30,
INPUT_KEY_2 = 31,
INPUT_KEY_3 = 32,
INPUT_KEY_4 = 33,
INPUT_KEY_5 = 34,
INPUT_KEY_6 = 35,
INPUT_KEY_7 = 36,
INPUT_KEY_8 = 37,
INPUT_KEY_9 = 38,
INPUT_KEY_0 = 39,
INPUT_KEY_RETURN = 40,
INPUT_KEY_ESCAPE = 41,
INPUT_KEY_BACKSPACE = 42,
INPUT_KEY_TAB = 43,
INPUT_KEY_SPACE = 44,
INPUT_KEY_MINUS = 45,
INPUT_KEY_EQUALS = 46,
INPUT_KEY_LEFTBRACKET = 47,
INPUT_KEY_RIGHTBRACKET = 48,
INPUT_KEY_BACKSLASH = 49,
INPUT_KEY_HASH = 50,
INPUT_KEY_SEMICOLON = 51,
INPUT_KEY_APOSTROPHE = 52,
INPUT_KEY_TILDE = 53,
INPUT_KEY_COMMA = 54,
INPUT_KEY_PERIOD = 55,
INPUT_KEY_SLASH = 56,
INPUT_KEY_CAPSLOCK = 57,
INPUT_KEY_F1 = 58,
INPUT_KEY_F2 = 59,
INPUT_KEY_F3 = 60,
INPUT_KEY_F4 = 61,
INPUT_KEY_F5 = 62,
INPUT_KEY_F6 = 63,
INPUT_KEY_F7 = 64,
INPUT_KEY_F8 = 65,
INPUT_KEY_F9 = 66,
INPUT_KEY_F10 = 67,
INPUT_KEY_F11 = 68,
INPUT_KEY_F12 = 69,
INPUT_KEY_PRINTSCREEN = 70,
INPUT_KEY_SCROLLLOCK = 71,
INPUT_KEY_PAUSE = 72,
INPUT_KEY_INSERT = 73,
INPUT_KEY_HOME = 74,
INPUT_KEY_PAGEUP = 75,
INPUT_KEY_DELETE = 76,
INPUT_KEY_END = 77,
INPUT_KEY_PAGEDOWN = 78,
INPUT_KEY_RIGHT = 79,
INPUT_KEY_LEFT = 80,
INPUT_KEY_DOWN = 81,
INPUT_KEY_UP = 82,
INPUT_KEY_NUMLOCK = 83,
INPUT_KEY_KP_DIVIDE = 84,
INPUT_KEY_KP_MULTIPLY = 85,
INPUT_KEY_KP_MINUS = 86,
INPUT_KEY_KP_PLUS = 87,
INPUT_KEY_KP_ENTER = 88,
INPUT_KEY_KP_1 = 89,
INPUT_KEY_KP_2 = 90,
INPUT_KEY_KP_3 = 91,
INPUT_KEY_KP_4 = 92,
INPUT_KEY_KP_5 = 93,
INPUT_KEY_KP_6 = 94,
INPUT_KEY_KP_7 = 95,
INPUT_KEY_KP_8 = 96,
INPUT_KEY_KP_9 = 97,
INPUT_KEY_KP_0 = 98,
INPUT_KEY_KP_PERIOD = 99,
INPUT_KEY_LCTRL = 100,
INPUT_KEY_LSHIFT = 101,
INPUT_KEY_LALT = 102,
INPUT_KEY_LGUI = 103,
INPUT_KEY_RCTRL = 104,
INPUT_KEY_RSHIFT = 105,
INPUT_KEY_RALT = 106,
INPUT_KEY_MAX = 107,
INPUT_GAMEPAD_A = 108,
INPUT_GAMEPAD_Y = 109,
INPUT_GAMEPAD_B = 110,
INPUT_GAMEPAD_X = 111,
INPUT_GAMEPAD_L_SHOULDER = 112,
INPUT_GAMEPAD_R_SHOULDER = 113,
INPUT_GAMEPAD_L_TRIGGER = 114,
INPUT_GAMEPAD_R_TRIGGER = 115,
INPUT_GAMEPAD_SELECT = 116,
INPUT_GAMEPAD_START = 117,
INPUT_GAMEPAD_L_STICK_PRESS = 118,
INPUT_GAMEPAD_R_STICK_PRESS = 119,
INPUT_GAMEPAD_DPAD_UP = 120,
INPUT_GAMEPAD_DPAD_DOWN = 121,
INPUT_GAMEPAD_DPAD_LEFT = 122,
INPUT_GAMEPAD_DPAD_RIGHT = 123,
INPUT_GAMEPAD_HOME = 124,
INPUT_GAMEPAD_L_STICK_UP = 125,
INPUT_GAMEPAD_L_STICK_DOWN = 126,
INPUT_GAMEPAD_L_STICK_LEFT = 127,
INPUT_GAMEPAD_L_STICK_RIGHT = 128,
INPUT_GAMEPAD_R_STICK_UP = 129,
INPUT_GAMEPAD_R_STICK_DOWN = 130,
INPUT_GAMEPAD_R_STICK_LEFT = 131,
INPUT_GAMEPAD_R_STICK_RIGHT = 132,
INPUT_MOUSE_LEFT = 134,
INPUT_MOUSE_MIDDLE = 135,
INPUT_MOUSE_RIGHT = 136,
INPUT_MOUSE_WHEEL_UP = 137,
INPUT_MOUSE_WHEEL_DOWN = 138,
INPUT_BUTTON_MAX = 139
} button_t;
typedef enum {
INPUT_LAYER_SYSTEM = 0,
INPUT_LAYER_USER = 1,
INPUT_LAYER_MAX = 2
} input_layer_t;
typedef void(*input_capture_callback_t)
(void *user, button_t button, int32_t ascii_char);
void input_init();
void input_cleanup();
void input_clear();
void input_bind(input_layer_t layer, button_t button, uint8_t action);
void input_unbind(input_layer_t layer,button_t button);
void input_unbind_all(input_layer_t layer);
uint8_t input_bound_to_action(button_t button);
void input_set_button_state(button_t button, float state);
void input_set_mouse_pos(int32_t x, int32_t y);
void input_textinput(int32_t ascii_char);
void input_capture(input_capture_callback_t cb, void *user);
float input_state(uint8_t action);
bool input_pressed(uint8_t action);
bool input_released(uint8_t action);
vec2_t input_mouse_pos();
button_t input_name_to_button(const char *name);
const char *input_button_to_name(button_t button);
#define INPUT_ACTION_COMMAND 31
#define INPUT_ACTION_MAX 32
#define INPUT_DEADZONE 0.1
#define INPUT_DEADZONE_CAPTURE 0.5
#define INPUT_ACTION_NONE 255
#define INPUT_BUTTON_NONE 0
#endif

File diff suppressed because it is too large Load Diff

@ -0,0 +1,728 @@
/*
Copyright (c) 2023, Dominic Szablewski - https://phoboslab.org
SPDX-License-Identifier: MIT
QOA - The "Quite OK Audio" format for fast, lossy audio compression
-- Data Format
QOA encodes pulse-code modulated (PCM) audio data with up to 255 channels,
sample rates from 1 up to 16777215 hertz and a bit depth of 16 bits.
The compression method employed in QOA is lossy; it discards some information
from the uncompressed PCM data. For many types of audio signals this compression
is "transparent", i.e. the difference from the original file is often not
audible.
QOA encodes 20 samples of 16 bit PCM data into slices of 64 bits. A single
sample therefore requires 3.2 bits of storage space, resulting in a 5x
compression (16 / 3.2).
A QOA file consists of an 8 byte file header, followed by a number of frames.
Each frame contains an 8 byte frame header, the current 16 byte en-/decoder
state per channel and 256 slices per channel. Each slice is 8 bytes wide and
encodes 20 samples of audio data.
All values, including the slices, are big endian. The file layout is as follows:
struct {
struct {
char magic[4]; // magic bytes "qoaf"
uint32_t samples; // samples per channel in this file
} file_header;
struct {
struct {
uint8_t num_channels; // no. of channels
uint24_t samplerate; // samplerate in hz
uint16_t fsamples; // samples per channel in this frame
uint16_t fsize; // frame size (includes this header)
} frame_header;
struct {
int16_t history[4]; // most recent last
int16_t weights[4]; // most recent last
} lms_state[num_channels];
qoa_slice_t slices[256][num_channels];
} frames[ceil(samples / (256 * 20))];
} qoa_file_t;
Each `qoa_slice_t` contains a quantized scalefactor `sf_quant` and 20 quantized
residuals `qrNN`:
.- QOA_SLICE -- 64 bits, 20 samples --------------------------/ /------------.
| Byte[0] | Byte[1] | Byte[2] \ \ Byte[7] |
| 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 | 7 6 5 / / 2 1 0 |
|------------+--------+--------+--------+---------+---------+-\ \--+---------|
| sf_quant | qr00 | qr01 | qr02 | qr03 | qr04 | / / | qr19 |
`-------------------------------------------------------------\ \------------`
Each frame except the last must contain exactly 256 slices per channel. The last
frame may contain between 1 .. 256 (inclusive) slices per channel. The last
slice (for each channel) in the last frame may contain less than 20 samples; the
slice still must be 8 bytes wide, with the unused samples zeroed out.
Channels are interleaved per slice. E.g. for 2 channel stereo:
slice[0] = L, slice[1] = R, slice[2] = L, slice[3] = R ...
A valid QOA file or stream must have at least one frame. Each frame must contain
at least one channel and one sample with a samplerate between 1 .. 16777215
(inclusive).
If the total number of samples is not known by the encoder, the samples in the
file header may be set to 0x00000000 to indicate that the encoder is
"streaming". In a streaming context, the samplerate and number of channels may
differ from frame to frame. For static files (those with samples set to a
non-zero value), each frame must have the same number of channels and same
samplerate.
Note that this implementation of QOA only handles files with a known total
number of samples.
A decoder should support at least 8 channels. The channel layout for channel
counts 1 .. 8 is:
1. Mono
2. L, R
3. L, R, C
4. FL, FR, B/SL, B/SR
5. FL, FR, C, B/SL, B/SR
6. FL, FR, C, LFE, B/SL, B/SR
7. FL, FR, C, LFE, B, SL, SR
8. FL, FR, C, LFE, BL, BR, SL, SR
QOA predicts each audio sample based on the previously decoded ones using a
"Sign-Sign Least Mean Squares Filter" (LMS). This prediction plus the
dequantized residual forms the final output sample.
*/
/* -----------------------------------------------------------------------------
Header - Public functions */
#ifndef QOA_H
#define QOA_H
#ifdef __cplusplus
extern "C" {
#endif
#define QOA_MIN_FILESIZE 16
#define QOA_MAX_CHANNELS 8
#define QOA_SLICE_LEN 20
#define QOA_SLICES_PER_FRAME 256
#define QOA_FRAME_LEN (QOA_SLICES_PER_FRAME * QOA_SLICE_LEN)
#define QOA_LMS_LEN 4
#define QOA_MAGIC 0x716f6166 /* 'qoaf' */
#define QOA_FRAME_SIZE(channels, slices) \
(8 + QOA_LMS_LEN * 4 * channels + 8 * slices * channels)
typedef struct {
int history[QOA_LMS_LEN];
int weights[QOA_LMS_LEN];
} qoa_lms_t;
typedef struct {
unsigned int channels;
unsigned int samplerate;
unsigned int samples;
qoa_lms_t lms[QOA_MAX_CHANNELS];
#ifdef QOA_RECORD_TOTAL_ERROR
double error;
#endif
} qoa_desc;
unsigned int qoa_encode_header(qoa_desc *qoa, unsigned char *bytes);
unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned int frame_len, unsigned char *bytes);
void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len);
unsigned int qoa_max_frame_size(qoa_desc *qoa);
unsigned int qoa_decode_header(const unsigned char *bytes, int size, qoa_desc *qoa);
unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa_desc *qoa, short *sample_data, unsigned int *frame_len);
short *qoa_decode(const unsigned char *bytes, int size, qoa_desc *file);
#ifndef QOA_NO_STDIO
int qoa_write(const char *filename, const short *sample_data, qoa_desc *qoa);
void *qoa_read(const char *filename, qoa_desc *qoa);
#endif /* QOA_NO_STDIO */
#ifdef __cplusplus
}
#endif
#endif /* QOA_H */
/* -----------------------------------------------------------------------------
Implementation */
#ifdef QOA_IMPLEMENTATION
#include <stdlib.h>
#ifndef QOA_MALLOC
#define QOA_MALLOC(sz) malloc(sz)
#define QOA_FREE(p) free(p)
#endif
typedef unsigned long long qoa_uint64_t;
/* The quant_tab provides an index into the dequant_tab for residuals in the
range of -8 .. 8. It maps this range to just 3bits and becomes less accurate at
the higher end. Note that the residual zero is identical to the lowest positive
value. This is mostly fine, since the qoa_div() function always rounds away
from zero. */
static const int qoa_quant_tab[17] = {
7, 7, 7, 5, 5, 3, 3, 1, /* -8..-1 */
0, /* 0 */
0, 2, 2, 4, 4, 6, 6, 6 /* 1.. 8 */
};
/* We have 16 different scalefactors. Like the quantized residuals these become
less accurate at the higher end. In theory, the highest scalefactor that we
would need to encode the highest 16bit residual is (2**16)/8 = 8192. However we
rely on the LMS filter to predict samples accurately enough that a maximum
residual of one quarter of the 16 bit range is sufficient. I.e. with the
scalefactor 2048 times the quant range of 8 we can encode residuals up to 2**14.
The scalefactor values are computed as:
scalefactor_tab[s] <- round(pow(s + 1, 2.75)) */
static const int qoa_scalefactor_tab[16] = {
1, 7, 21, 45, 84, 138, 211, 304, 421, 562, 731, 928, 1157, 1419, 1715, 2048
};
/* The reciprocal_tab maps each of the 16 scalefactors to their rounded
reciprocals 1/scalefactor. This allows us to calculate the scaled residuals in
the encoder with just one multiplication instead of an expensive division. We
do this in .16 fixed point with integers, instead of floats.
The reciprocal_tab is computed as:
reciprocal_tab[s] <- ((1<<16) + scalefactor_tab[s] - 1) / scalefactor_tab[s] */
static const int qoa_reciprocal_tab[16] = {
65536, 9363, 3121, 1457, 781, 475, 311, 216, 156, 117, 90, 71, 57, 47, 39, 32
};
/* The dequant_tab maps each of the scalefactors and quantized residuals to
their unscaled & dequantized version.
Since qoa_div rounds away from the zero, the smallest entries are mapped to 3/4
instead of 1. The dequant_tab assumes the following dequantized values for each
of the quant_tab indices and is computed as:
float dqt[8] = {0.75, -0.75, 2.5, -2.5, 4.5, -4.5, 7, -7};
dequant_tab[s][q] <- round_ties_away_from_zero(scalefactor_tab[s] * dqt[q])
The rounding employed here is "to nearest, ties away from zero", i.e. positive
and negative values are treated symmetrically.
*/
static const int qoa_dequant_tab[16][8] = {
{ 1, -1, 3, -3, 5, -5, 7, -7},
{ 5, -5, 18, -18, 32, -32, 49, -49},
{ 16, -16, 53, -53, 95, -95, 147, -147},
{ 34, -34, 113, -113, 203, -203, 315, -315},
{ 63, -63, 210, -210, 378, -378, 588, -588},
{ 104, -104, 345, -345, 621, -621, 966, -966},
{ 158, -158, 528, -528, 950, -950, 1477, -1477},
{ 228, -228, 760, -760, 1368, -1368, 2128, -2128},
{ 316, -316, 1053, -1053, 1895, -1895, 2947, -2947},
{ 422, -422, 1405, -1405, 2529, -2529, 3934, -3934},
{ 548, -548, 1828, -1828, 3290, -3290, 5117, -5117},
{ 696, -696, 2320, -2320, 4176, -4176, 6496, -6496},
{ 868, -868, 2893, -2893, 5207, -5207, 8099, -8099},
{1064, -1064, 3548, -3548, 6386, -6386, 9933, -9933},
{1286, -1286, 4288, -4288, 7718, -7718, 12005, -12005},
{1536, -1536, 5120, -5120, 9216, -9216, 14336, -14336},
};
/* The Least Mean Squares Filter is the heart of QOA. It predicts the next
sample based on the previous 4 reconstructed samples. It does so by continuously
adjusting 4 weights based on the residual of the previous prediction.
The next sample is predicted as the sum of (weight[i] * history[i]).
The adjustment of the weights is done with a "Sign-Sign-LMS" that adds or
subtracts the residual to each weight, based on the corresponding sample from
the history. This, surprisingly, is sufficient to get worthwhile predictions.
This is all done with fixed point integers. Hence the right-shifts when updating
the weights and calculating the prediction. */
static int qoa_lms_predict(qoa_lms_t *lms) {
int prediction = 0;
for (int i = 0; i < QOA_LMS_LEN; i++) {
prediction += lms->weights[i] * lms->history[i];
}
return prediction >> 13;
}
static void qoa_lms_update(qoa_lms_t *lms, int sample, int residual) {
int delta = residual >> 4;
for (int i = 0; i < QOA_LMS_LEN; i++) {
lms->weights[i] += lms->history[i] < 0 ? -delta : delta;
}
for (int i = 0; i < QOA_LMS_LEN-1; i++) {
lms->history[i] = lms->history[i+1];
}
lms->history[QOA_LMS_LEN-1] = sample;
}
/* qoa_div() implements a rounding division, but avoids rounding to zero for
small numbers. E.g. 0.1 will be rounded to 1. Note that 0 itself still
returns as 0, which is handled in the qoa_quant_tab[].
qoa_div() takes an index into the .16 fixed point qoa_reciprocal_tab as an
argument, so it can do the division with a cheaper integer multiplication. */
static inline int qoa_div(int v, int scalefactor) {
int reciprocal = qoa_reciprocal_tab[scalefactor];
int n = (v * reciprocal + (1 << 15)) >> 16;
n = n + ((v > 0) - (v < 0)) - ((n > 0) - (n < 0)); /* round away from 0 */
return n;
}
static inline int qoa_clamp(int v, int min, int max) {
if (v < min) { return min; }
if (v > max) { return max; }
return v;
}
/* This specialized clamp function for the signed 16 bit range improves decode
performance quite a bit. The extra if() statement works nicely with the CPUs
branch prediction as this branch is rarely taken. */
static inline int qoa_clamp_s16(int v) {
if ((unsigned int)(v + 32768) > 65535) {
if (v < -32768) { return -32768; }
if (v > 32767) { return 32767; }
}
return v;
}
static inline qoa_uint64_t qoa_read_u64(const unsigned char *bytes, unsigned int *p) {
bytes += *p;
*p += 8;
return
((qoa_uint64_t)(bytes[0]) << 56) | ((qoa_uint64_t)(bytes[1]) << 48) |
((qoa_uint64_t)(bytes[2]) << 40) | ((qoa_uint64_t)(bytes[3]) << 32) |
((qoa_uint64_t)(bytes[4]) << 24) | ((qoa_uint64_t)(bytes[5]) << 16) |
((qoa_uint64_t)(bytes[6]) << 8) | ((qoa_uint64_t)(bytes[7]) << 0);
}
static inline void qoa_write_u64(qoa_uint64_t v, unsigned char *bytes, unsigned int *p) {
bytes += *p;
*p += 8;
bytes[0] = (v >> 56) & 0xff;
bytes[1] = (v >> 48) & 0xff;
bytes[2] = (v >> 40) & 0xff;
bytes[3] = (v >> 32) & 0xff;
bytes[4] = (v >> 24) & 0xff;
bytes[5] = (v >> 16) & 0xff;
bytes[6] = (v >> 8) & 0xff;
bytes[7] = (v >> 0) & 0xff;
}
/* -----------------------------------------------------------------------------
Encoder */
unsigned int qoa_encode_header(qoa_desc *qoa, unsigned char *bytes) {
unsigned int p = 0;
qoa_write_u64(((qoa_uint64_t)QOA_MAGIC << 32) | qoa->samples, bytes, &p);
return p;
}
unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned int frame_len, unsigned char *bytes) {
unsigned int channels = qoa->channels;
unsigned int p = 0;
unsigned int slices = (frame_len + QOA_SLICE_LEN - 1) / QOA_SLICE_LEN;
unsigned int frame_size = QOA_FRAME_SIZE(channels, slices);
int prev_scalefactor[QOA_MAX_CHANNELS] = {0};
/* Write the frame header */
qoa_write_u64((
(qoa_uint64_t)qoa->channels << 56 |
(qoa_uint64_t)qoa->samplerate << 32 |
(qoa_uint64_t)frame_len << 16 |
(qoa_uint64_t)frame_size
), bytes, &p);
for (int c = 0; c < channels; c++) {
/* If the weights have grown too large, reset them to 0. This may happen
with certain high-frequency sounds. This is a last resort and will
introduce quite a bit of noise, but should at least prevent pops/clicks */
int weights_sum =
qoa->lms[c].weights[0] * qoa->lms[c].weights[0] +
qoa->lms[c].weights[1] * qoa->lms[c].weights[1] +
qoa->lms[c].weights[2] * qoa->lms[c].weights[2] +
qoa->lms[c].weights[3] * qoa->lms[c].weights[3];
if (weights_sum > 0x2fffffff) {
qoa->lms[c].weights[0] = 0;
qoa->lms[c].weights[1] = 0;
qoa->lms[c].weights[2] = 0;
qoa->lms[c].weights[3] = 0;
}
/* Write the current LMS state */
qoa_uint64_t weights = 0;
qoa_uint64_t history = 0;
for (int i = 0; i < QOA_LMS_LEN; i++) {
history = (history << 16) | (qoa->lms[c].history[i] & 0xffff);
weights = (weights << 16) | (qoa->lms[c].weights[i] & 0xffff);
}
qoa_write_u64(history, bytes, &p);
qoa_write_u64(weights, bytes, &p);
}
/* We encode all samples with the channels interleaved on a slice level.
E.g. for stereo: (ch-0, slice 0), (ch 1, slice 0), (ch 0, slice 1), ...*/
for (int sample_index = 0; sample_index < frame_len; sample_index += QOA_SLICE_LEN) {
for (int c = 0; c < channels; c++) {
int slice_len = qoa_clamp(QOA_SLICE_LEN, 0, frame_len - sample_index);
int slice_start = sample_index * channels + c;
int slice_end = (sample_index + slice_len) * channels + c;
/* Brute for search for the best scalefactor. Just go through all
16 scalefactors, encode all samples for the current slice and
meassure the total squared error. */
qoa_uint64_t best_error = -1;
qoa_uint64_t best_slice;
qoa_lms_t best_lms;
int best_scalefactor;
for (int sfi = 0; sfi < 16; sfi++) {
/* There is a strong correlation between the scalefactors of
neighboring slices. As an optimization, start testing
the best scalefactor of the previous slice first. */
int scalefactor = (sfi + prev_scalefactor[c]) % 16;
/* We have to reset the LMS state to the last known good one
before trying each scalefactor, as each pass updates the LMS
state when encoding. */
qoa_lms_t lms = qoa->lms[c];
qoa_uint64_t slice = scalefactor;
qoa_uint64_t current_error = 0;
for (int si = slice_start; si < slice_end; si += channels) {
int sample = sample_data[si];
int predicted = qoa_lms_predict(&lms);
int residual = sample - predicted;
int scaled = qoa_div(residual, scalefactor);
int clamped = qoa_clamp(scaled, -8, 8);
int quantized = qoa_quant_tab[clamped + 8];
int dequantized = qoa_dequant_tab[scalefactor][quantized];
int reconstructed = qoa_clamp_s16(predicted + dequantized);
long long error = (sample - reconstructed);
current_error += error * error;
if (current_error > best_error) {
break;
}
qoa_lms_update(&lms, reconstructed, dequantized);
slice = (slice << 3) | quantized;
}
if (current_error < best_error) {
best_error = current_error;
best_slice = slice;
best_lms = lms;
best_scalefactor = scalefactor;
}
}
prev_scalefactor[c] = best_scalefactor;
qoa->lms[c] = best_lms;
#ifdef QOA_RECORD_TOTAL_ERROR
qoa->error += best_error;
#endif
/* If this slice was shorter than QOA_SLICE_LEN, we have to left-
shift all encoded data, to ensure the rightmost bits are the empty
ones. This should only happen in the last frame of a file as all
slices are completely filled otherwise. */
best_slice <<= (QOA_SLICE_LEN - slice_len) * 3;
qoa_write_u64(best_slice, bytes, &p);
}
}
return p;
}
void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len) {
if (
qoa->samples == 0 ||
qoa->samplerate == 0 || qoa->samplerate > 0xffffff ||
qoa->channels == 0 || qoa->channels > QOA_MAX_CHANNELS
) {
return NULL;
}
/* Calculate the encoded size and allocate */
unsigned int num_frames = (qoa->samples + QOA_FRAME_LEN-1) / QOA_FRAME_LEN;
unsigned int num_slices = (qoa->samples + QOA_SLICE_LEN-1) / QOA_SLICE_LEN;
unsigned int encoded_size = 8 + /* 8 byte file header */
num_frames * 8 + /* 8 byte frame headers */
num_frames * QOA_LMS_LEN * 4 * qoa->channels + /* 4 * 4 bytes lms state per channel */
num_slices * 8 * qoa->channels; /* 8 byte slices */
unsigned char *bytes = QOA_MALLOC(encoded_size);
for (int c = 0; c < qoa->channels; c++) {
/* Set the initial LMS weights to {0, 0, -1, 2}. This helps with the
prediction of the first few ms of a file. */
qoa->lms[c].weights[0] = 0;
qoa->lms[c].weights[1] = 0;
qoa->lms[c].weights[2] = -(1<<13);
qoa->lms[c].weights[3] = (1<<14);
/* Explicitly set the history samples to 0, as we might have some
garbage in there. */
for (int i = 0; i < QOA_LMS_LEN; i++) {
qoa->lms[c].history[i] = 0;
}
}
/* Encode the header and go through all frames */
unsigned int p = qoa_encode_header(qoa, bytes);
#ifdef QOA_RECORD_TOTAL_ERROR
qoa->error = 0;
#endif
int frame_len = QOA_FRAME_LEN;
for (int sample_index = 0; sample_index < qoa->samples; sample_index += frame_len) {
frame_len = qoa_clamp(QOA_FRAME_LEN, 0, qoa->samples - sample_index);
const short *frame_samples = sample_data + sample_index * qoa->channels;
unsigned int frame_size = qoa_encode_frame(frame_samples, qoa, frame_len, bytes + p);
p += frame_size;
}
*out_len = p;
return bytes;
}
/* -----------------------------------------------------------------------------
Decoder */
unsigned int qoa_max_frame_size(qoa_desc *qoa) {
return QOA_FRAME_SIZE(qoa->channels, QOA_SLICES_PER_FRAME);
}
unsigned int qoa_decode_header(const unsigned char *bytes, int size, qoa_desc *qoa) {
unsigned int p = 0;
if (size < QOA_MIN_FILESIZE) {
return 0;
}
/* Read the file header, verify the magic number ('qoaf') and read the
total number of samples. */
qoa_uint64_t file_header = qoa_read_u64(bytes, &p);
if ((file_header >> 32) != QOA_MAGIC) {
return 0;
}
qoa->samples = file_header & 0xffffffff;
if (!qoa->samples) {
return 0;
}
/* Peek into the first frame header to get the number of channels and
the samplerate. */
qoa_uint64_t frame_header = qoa_read_u64(bytes, &p);
qoa->channels = (frame_header >> 56) & 0x0000ff;
qoa->samplerate = (frame_header >> 32) & 0xffffff;
if (qoa->channels == 0 || qoa->samples == 0 || qoa->samplerate == 0) {
return 0;
}
return 8;
}
unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa_desc *qoa, short *sample_data, unsigned int *frame_len) {
unsigned int p = 0;
*frame_len = 0;
if (size < 8 + QOA_LMS_LEN * 4 * qoa->channels) {
return 0;
}
/* Read and verify the frame header */
qoa_uint64_t frame_header = qoa_read_u64(bytes, &p);
int channels = (frame_header >> 56) & 0x0000ff;
int samplerate = (frame_header >> 32) & 0xffffff;
int samples = (frame_header >> 16) & 0x00ffff;
int frame_size = (frame_header ) & 0x00ffff;
int data_size = frame_size - 8 - QOA_LMS_LEN * 4 * channels;
int num_slices = data_size / 8;
int max_total_samples = num_slices * QOA_SLICE_LEN;
if (
channels != qoa->channels ||
samplerate != qoa->samplerate ||
frame_size > size ||
samples * channels > max_total_samples
) {
return 0;
}
/* Read the LMS state: 4 x 2 bytes history, 4 x 2 bytes weights per channel */
for (int c = 0; c < channels; c++) {
qoa_uint64_t history = qoa_read_u64(bytes, &p);
qoa_uint64_t weights = qoa_read_u64(bytes, &p);
for (int i = 0; i < QOA_LMS_LEN; i++) {
qoa->lms[c].history[i] = ((signed short)(history >> 48));
history <<= 16;
qoa->lms[c].weights[i] = ((signed short)(weights >> 48));
weights <<= 16;
}
}
/* Decode all slices for all channels in this frame */
for (int sample_index = 0; sample_index < samples; sample_index += QOA_SLICE_LEN) {
for (int c = 0; c < channels; c++) {
qoa_uint64_t slice = qoa_read_u64(bytes, &p);
int scalefactor = (slice >> 60) & 0xf;
int slice_start = sample_index * channels + c;
int slice_end = qoa_clamp(sample_index + QOA_SLICE_LEN, 0, samples) * channels + c;
for (int si = slice_start; si < slice_end; si += channels) {
int predicted = qoa_lms_predict(&qoa->lms[c]);
int quantized = (slice >> 57) & 0x7;
int dequantized = qoa_dequant_tab[scalefactor][quantized];
int reconstructed = qoa_clamp_s16(predicted + dequantized);
sample_data[si] = reconstructed;
slice <<= 3;
qoa_lms_update(&qoa->lms[c], reconstructed, dequantized);
}
}
}
*frame_len = samples;
return p;
}
short *qoa_decode(const unsigned char *bytes, int size, qoa_desc *qoa) {
unsigned int p = qoa_decode_header(bytes, size, qoa);
if (!p) {
return NULL;
}
/* Calculate the required size of the sample buffer and allocate */
int total_samples = qoa->samples * qoa->channels;
short *sample_data = QOA_MALLOC(total_samples * sizeof(short));
unsigned int sample_index = 0;
unsigned int frame_len;
unsigned int frame_size;
/* Decode all frames */
do {
short *sample_ptr = sample_data + sample_index * qoa->channels;
frame_size = qoa_decode_frame(bytes + p, size - p, qoa, sample_ptr, &frame_len);
p += frame_size;
sample_index += frame_len;
} while (frame_size && sample_index < qoa->samples);
qoa->samples = sample_index;
return sample_data;
}
/* -----------------------------------------------------------------------------
File read/write convenience functions */
#ifndef QOA_NO_STDIO
#include <stdio.h>
int qoa_write(const char *filename, const short *sample_data, qoa_desc *qoa) {
FILE *f = fopen(filename, "wb");
unsigned int size;
void *encoded;
if (!f) {
return 0;
}
encoded = qoa_encode(sample_data, qoa, &size);
if (!encoded) {
fclose(f);
return 0;
}
fwrite(encoded, 1, size, f);
fclose(f);
QOA_FREE(encoded);
return size;
}
void *qoa_read(const char *filename, qoa_desc *qoa) {
FILE *f = fopen(filename, "rb");
int size, bytes_read;
void *data;
short *sample_data;
if (!f) {
return NULL;
}
fseek(f, 0, SEEK_END);
size = ftell(f);
if (size <= 0) {
fclose(f);
return NULL;
}
fseek(f, 0, SEEK_SET);
data = QOA_MALLOC(size);
if (!data) {
fclose(f);
return NULL;
}
bytes_read = fread(data, 1, size, f);
fclose(f);
sample_data = qoa_decode(data, bytes_read, qoa);
QOA_FREE(data);
return sample_data;
}
#endif /* QOA_NO_STDIO */
#endif /* QOA_IMPLEMENTATION */

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,324 @@
#if defined(SOKOL_IMPL) && !defined(SOKOL_TIME_IMPL)
#define SOKOL_TIME_IMPL
#endif
#ifndef SOKOL_TIME_INCLUDED
/*
sokol_time.h -- simple cross-platform time measurement
Project URL: https://github.com/floooh/sokol
Do this:
#define SOKOL_IMPL or
#define SOKOL_TIME_IMPL
before you include this file in *one* C or C++ file to create the
implementation.
Optionally provide the following defines with your own implementations:
SOKOL_ASSERT(c) - your own assert macro (default: assert(c))
SOKOL_TIME_API_DECL - public function declaration prefix (default: extern)
SOKOL_API_DECL - same as SOKOL_TIME_API_DECL
SOKOL_API_IMPL - public function implementation prefix (default: -)
If sokol_time.h is compiled as a DLL, define the following before
including the declaration or implementation:
SOKOL_DLL
On Windows, SOKOL_DLL will define SOKOL_TIME_API_DECL as __declspec(dllexport)
or __declspec(dllimport) as needed.
void stm_setup();
Call once before any other functions to initialize sokol_time
(this calls for instance QueryPerformanceFrequency on Windows)
uint64_t stm_now();
Get current point in time in unspecified 'ticks'. The value that
is returned has no relation to the 'wall-clock' time and is
not in a specific time unit, it is only useful to compute
time differences.
uint64_t stm_diff(uint64_t new, uint64_t old);
Computes the time difference between new and old. This will always
return a positive, non-zero value.
uint64_t stm_since(uint64_t start);
Takes the current time, and returns the elapsed time since start
(this is a shortcut for "stm_diff(stm_now(), start)")
uint64_t stm_laptime(uint64_t* last_time);
This is useful for measuring frame time and other recurring
events. It takes the current time, returns the time difference
to the value in last_time, and stores the current time in
last_time for the next call. If the value in last_time is 0,
the return value will be zero (this usually happens on the
very first call).
uint64_t stm_round_to_common_refresh_rate(uint64_t duration)
This oddly named function takes a measured frame time and
returns the closest "nearby" common display refresh rate frame duration
in ticks. If the input duration isn't close to any common display
refresh rate, the input duration will be returned unchanged as a fallback.
The main purpose of this function is to remove jitter/inaccuracies from
measured frame times, and instead use the display refresh rate as
frame duration.
Use the following functions to convert a duration in ticks into
useful time units:
double stm_sec(uint64_t ticks);
double stm_ms(uint64_t ticks);
double stm_us(uint64_t ticks);
double stm_ns(uint64_t ticks);
Converts a tick value into seconds, milliseconds, microseconds
or nanoseconds. Note that not all platforms will have nanosecond
or even microsecond precision.
Uses the following time measurement functions under the hood:
Windows: QueryPerformanceFrequency() / QueryPerformanceCounter()
MacOS/iOS: mach_absolute_time()
emscripten: performance.now()
Linux+others: clock_gettime(CLOCK_MONOTONIC)
zlib/libpng license
Copyright (c) 2018 Andre Weissflog
This software is provided 'as-is', without any express or implied warranty.
In no event will the authors be held liable for any damages arising from the
use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software in a
product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not
be misrepresented as being the original software.
3. This notice may not be removed or altered from any source
distribution.
*/
#define SOKOL_TIME_INCLUDED (1)
#include <stdint.h>
#if defined(SOKOL_API_DECL) && !defined(SOKOL_TIME_API_DECL)
#define SOKOL_TIME_API_DECL SOKOL_API_DECL
#endif
#ifndef SOKOL_TIME_API_DECL
#if defined(_WIN32) && defined(SOKOL_DLL) && defined(SOKOL_TIME_IMPL)
#define SOKOL_TIME_API_DECL __declspec(dllexport)
#elif defined(_WIN32) && defined(SOKOL_DLL)
#define SOKOL_TIME_API_DECL __declspec(dllimport)
#else
#define SOKOL_TIME_API_DECL extern
#endif
#endif
#ifdef __cplusplus
extern "C" {
#endif
SOKOL_TIME_API_DECL void stm_setup(void);
SOKOL_TIME_API_DECL uint64_t stm_now(void);
SOKOL_TIME_API_DECL uint64_t stm_diff(uint64_t new_ticks, uint64_t old_ticks);
SOKOL_TIME_API_DECL uint64_t stm_since(uint64_t start_ticks);
SOKOL_TIME_API_DECL uint64_t stm_laptime(uint64_t* last_time);
SOKOL_TIME_API_DECL uint64_t stm_round_to_common_refresh_rate(uint64_t frame_ticks);
SOKOL_TIME_API_DECL double stm_sec(uint64_t ticks);
SOKOL_TIME_API_DECL double stm_ms(uint64_t ticks);
SOKOL_TIME_API_DECL double stm_us(uint64_t ticks);
SOKOL_TIME_API_DECL double stm_ns(uint64_t ticks);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif // SOKOL_TIME_INCLUDED
/*-- IMPLEMENTATION ----------------------------------------------------------*/
#ifdef SOKOL_TIME_IMPL
#define SOKOL_TIME_IMPL_INCLUDED (1)
#include <string.h> /* memset */
#ifndef SOKOL_API_IMPL
#define SOKOL_API_IMPL
#endif
#ifndef SOKOL_ASSERT
#include <assert.h>
#define SOKOL_ASSERT(c) assert(c)
#endif
#ifndef _SOKOL_PRIVATE
#if defined(__GNUC__) || defined(__clang__)
#define _SOKOL_PRIVATE __attribute__((unused)) static
#else
#define _SOKOL_PRIVATE static
#endif
#endif
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
typedef struct {
uint32_t initialized;
LARGE_INTEGER freq;
LARGE_INTEGER start;
} _stm_state_t;
#elif defined(__APPLE__) && defined(__MACH__)
#include <mach/mach_time.h>
typedef struct {
uint32_t initialized;
mach_timebase_info_data_t timebase;
uint64_t start;
} _stm_state_t;
#elif defined(__EMSCRIPTEN__)
#include <emscripten/emscripten.h>
typedef struct {
uint32_t initialized;
double start;
} _stm_state_t;
#else /* anything else, this will need more care for non-Linux platforms */
#ifdef ESP8266
// On the ESP8266, clock_gettime ignores the first argument and CLOCK_MONOTONIC isn't defined
#define CLOCK_MONOTONIC 0
#endif
#include <time.h>
typedef struct {
uint32_t initialized;
uint64_t start;
} _stm_state_t;
#endif
static _stm_state_t _stm;
/* prevent 64-bit overflow when computing relative timestamp
see https://gist.github.com/jspohr/3dc4f00033d79ec5bdaf67bc46c813e3
*/
#if defined(_WIN32) || (defined(__APPLE__) && defined(__MACH__))
_SOKOL_PRIVATE int64_t int64_muldiv(int64_t value, int64_t numer, int64_t denom) {
int64_t q = value / denom;
int64_t r = value % denom;
return q * numer + r * numer / denom;
}
#endif
#if defined(__EMSCRIPTEN__)
EM_JS(double, stm_js_perfnow, (void), {
return performance.now();
});
#endif
SOKOL_API_IMPL void stm_setup(void) {
memset(&_stm, 0, sizeof(_stm));
_stm.initialized = 0xABCDABCD;
#if defined(_WIN32)
QueryPerformanceFrequency(&_stm.freq);
QueryPerformanceCounter(&_stm.start);
#elif defined(__APPLE__) && defined(__MACH__)
mach_timebase_info(&_stm.timebase);
_stm.start = mach_absolute_time();
#elif defined(__EMSCRIPTEN__)
_stm.start = stm_js_perfnow();
#else
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
_stm.start = (uint64_t)ts.tv_sec*1000000000 + (uint64_t)ts.tv_nsec;
#endif
}
SOKOL_API_IMPL uint64_t stm_now(void) {
SOKOL_ASSERT(_stm.initialized == 0xABCDABCD);
uint64_t now;
#if defined(_WIN32)
LARGE_INTEGER qpc_t;
QueryPerformanceCounter(&qpc_t);
now = (uint64_t) int64_muldiv(qpc_t.QuadPart - _stm.start.QuadPart, 1000000000, _stm.freq.QuadPart);
#elif defined(__APPLE__) && defined(__MACH__)
const uint64_t mach_now = mach_absolute_time() - _stm.start;
now = (uint64_t) int64_muldiv((int64_t)mach_now, (int64_t)_stm.timebase.numer, (int64_t)_stm.timebase.denom);
#elif defined(__EMSCRIPTEN__)
double js_now = stm_js_perfnow() - _stm.start;
SOKOL_ASSERT(js_now >= 0.0);
now = (uint64_t) (js_now * 1000000.0);
#else
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
now = ((uint64_t)ts.tv_sec*1000000000 + (uint64_t)ts.tv_nsec) - _stm.start;
#endif
return now;
}
SOKOL_API_IMPL uint64_t stm_diff(uint64_t new_ticks, uint64_t old_ticks) {
if (new_ticks > old_ticks) {
return new_ticks - old_ticks;
}
else {
return 1;
}
}
SOKOL_API_IMPL uint64_t stm_since(uint64_t start_ticks) {
return stm_diff(stm_now(), start_ticks);
}
SOKOL_API_IMPL uint64_t stm_laptime(uint64_t* last_time) {
SOKOL_ASSERT(last_time);
uint64_t dt = 0;
uint64_t now = stm_now();
if (0 != *last_time) {
dt = stm_diff(now, *last_time);
}
*last_time = now;
return dt;
}
// first number is frame duration in ns, second number is tolerance in ns,
// the resulting min/max values must not overlap!
static const uint64_t _stm_refresh_rates[][2] = {
{ 16666667, 1000000 }, // 60 Hz: 16.6667 +- 1ms
{ 13888889, 250000 }, // 72 Hz: 13.8889 +- 0.25ms
{ 13333333, 250000 }, // 75 Hz: 13.3333 +- 0.25ms
{ 11764706, 250000 }, // 85 Hz: 11.7647 +- 0.25
{ 11111111, 250000 }, // 90 Hz: 11.1111 +- 0.25ms
{ 10000000, 500000 }, // 100 Hz: 10.0000 +- 0.5ms
{ 8333333, 500000 }, // 120 Hz: 8.3333 +- 0.5ms
{ 6944445, 500000 }, // 144 Hz: 6.9445 +- 0.5ms
{ 4166667, 1000000 }, // 240 Hz: 4.1666 +- 1ms
{ 0, 0 }, // keep the last element always at zero
};
SOKOL_API_IMPL uint64_t stm_round_to_common_refresh_rate(uint64_t ticks) {
uint64_t ns;
int i = 0;
while (0 != (ns = _stm_refresh_rates[i][0])) {
uint64_t tol = _stm_refresh_rates[i][1];
if ((ticks > (ns - tol)) && (ticks < (ns + tol))) {
return ns;
}
i++;
}
// fallthough: didn't fit into any buckets
return ticks;
}
SOKOL_API_IMPL double stm_sec(uint64_t ticks) {
return (double)ticks / 1000000000.0;
}
SOKOL_API_IMPL double stm_ms(uint64_t ticks) {
return (double)ticks / 1000000.0;
}
SOKOL_API_IMPL double stm_us(uint64_t ticks) {
return (double)ticks / 1000.0;
}
SOKOL_API_IMPL double stm_ns(uint64_t ticks) {
return (double)ticks;
}
#endif /* SOKOL_TIME_IMPL */

File diff suppressed because it is too large Load Diff

@ -0,0 +1,81 @@
#include <stdlib.h>
#include <string.h>
#include "mem.h"
#include "utils.h"
static uint8_t hunk[MEM_HUNK_BYTES];
static uint32_t bump_len = 0;
static uint32_t temp_len = 0;
static uint32_t temp_objects[MEM_TEMP_OBJECTS_MAX] = {};
static uint32_t temp_objects_len;
// Bump allocator - returns bytes from the front of the hunk
// These allocations persist for many frames. The allocator level is reset
// whenever we load a new race track or menu in game_set_scene()
void *mem_mark() {
return &hunk[bump_len];
}
void *mem_bump(uint32_t size) {
error_if(bump_len + temp_len + size >= MEM_HUNK_BYTES, "Failed to allocate %d bytes in hunk mem", size);
uint8_t *p = &hunk[bump_len];
bump_len += size;
memset(p, 0, size);
return p;
}
void mem_reset(void *p) {
uint32_t offset = (uint8_t *)p - (uint8_t *)hunk;
error_if(offset > bump_len || offset > MEM_HUNK_BYTES, "Invalid mem reset");
bump_len = offset;
}
// Temp allocator - returns bytes from the back of the hunk
// Temporary allocated bytes are not allowed to persist for multiple frames. You
// need to explicitly free them when you are done. Temp allocated bytes don't
// have be freed in reverse allocation order. I.e. you can allocate A then B,
// and aftewards free A then B.
void *mem_temp_alloc(uint32_t size) {
size = ((size >> 3) + 7) << 3; // allign to 8 bytes
error_if(bump_len + temp_len + size >= MEM_HUNK_BYTES, "Failed to allocate %d bytes in temp mem", size);
error_if(temp_objects_len >= MEM_TEMP_OBJECTS_MAX, "MEM_TEMP_OBJECTS_MAX reached");
temp_len += size;
void *p = &hunk[MEM_HUNK_BYTES - temp_len];
temp_objects[temp_objects_len++] = temp_len;
return p;
}
void mem_temp_free(void *p) {
uint32_t offset = (uint8_t *)&hunk[MEM_HUNK_BYTES] - (uint8_t *)p;
error_if(offset > MEM_HUNK_BYTES, "Object 0x%p not in temp hunk", p);
bool found = false;
uint32_t remaining_max = 0;
for (int i = 0; i < temp_objects_len; i++) {
if (temp_objects[i] == offset) {
temp_objects[i--] = temp_objects[--temp_objects_len];
found = true;
}
else if (temp_objects[i] > remaining_max) {
remaining_max = temp_objects[i];
}
}
error_if(!found, "Object 0x%p not in temp hunk", p);
temp_len = remaining_max;
}
void mem_temp_check() {
error_if(temp_len != 0, "Temp memory not free: %d object(s)", temp_objects_len);
}

@ -0,0 +1,17 @@
#ifndef MEM_H
#define MEM_H
#include "types.h"
#define MEM_TEMP_OBJECTS_MAX 8
#define MEM_HUNK_BYTES (4 * 1024 * 1024)
void *mem_bump(uint32_t size);
void *mem_mark();
void mem_reset(void *p);
void *mem_temp_alloc(uint32_t size);
void mem_temp_free(void *p);
void mem_temp_check();
#endif

@ -0,0 +1,16 @@
#ifndef PLATFORM_H
#define PLATFORM_H
#include "types.h"
void platform_exit();
vec2i_t platform_screen_size();
double platform_now();
void platform_set_fullscreen(bool fullscreen);
void platform_set_audio_mix_cb(void (*cb)(float *buffer, uint32_t len));
#if defined(RENDERER_SOFTWARE)
rgba_t *platform_get_screenbuffer(int32_t *pitch);
#endif
#endif

@ -0,0 +1,350 @@
#include <SDL2/SDL.h>
#include "platform.h"
#include "input.h"
#include "system.h"
static uint64_t perf_freq = 0;
static bool wants_to_exit = false;
static SDL_Window *window;
static SDL_AudioDeviceID audio_device;
static SDL_GameController *gamepad;
static void (*audio_callback)(float *buffer, uint32_t len) = NULL;
uint8_t platform_sdl_gamepad_map[] = {
[SDL_CONTROLLER_BUTTON_A] = INPUT_GAMEPAD_A,
[SDL_CONTROLLER_BUTTON_B] = INPUT_GAMEPAD_B,
[SDL_CONTROLLER_BUTTON_X] = INPUT_GAMEPAD_X,
[SDL_CONTROLLER_BUTTON_Y] = INPUT_GAMEPAD_Y,
[SDL_CONTROLLER_BUTTON_BACK] = INPUT_GAMEPAD_SELECT,
[SDL_CONTROLLER_BUTTON_GUIDE] = INPUT_GAMEPAD_HOME,
[SDL_CONTROLLER_BUTTON_START] = INPUT_GAMEPAD_START,
[SDL_CONTROLLER_BUTTON_LEFTSTICK] = INPUT_GAMEPAD_L_STICK_PRESS,
[SDL_CONTROLLER_BUTTON_RIGHTSTICK] = INPUT_GAMEPAD_R_STICK_PRESS,
[SDL_CONTROLLER_BUTTON_LEFTSHOULDER] = INPUT_GAMEPAD_L_SHOULDER,
[SDL_CONTROLLER_BUTTON_RIGHTSHOULDER] = INPUT_GAMEPAD_R_SHOULDER,
[SDL_CONTROLLER_BUTTON_DPAD_UP] = INPUT_GAMEPAD_DPAD_UP,
[SDL_CONTROLLER_BUTTON_DPAD_DOWN] = INPUT_GAMEPAD_DPAD_DOWN,
[SDL_CONTROLLER_BUTTON_DPAD_LEFT] = INPUT_GAMEPAD_DPAD_LEFT,
[SDL_CONTROLLER_BUTTON_DPAD_RIGHT] = INPUT_GAMEPAD_DPAD_RIGHT,
[SDL_CONTROLLER_BUTTON_MAX] = INPUT_INVALID
};
uint8_t platform_sdl_axis_map[] = {
[SDL_CONTROLLER_AXIS_LEFTX] = INPUT_GAMEPAD_L_STICK_LEFT,
[SDL_CONTROLLER_AXIS_LEFTY] = INPUT_GAMEPAD_L_STICK_UP,
[SDL_CONTROLLER_AXIS_RIGHTX] = INPUT_GAMEPAD_R_STICK_LEFT,
[SDL_CONTROLLER_AXIS_RIGHTY] = INPUT_GAMEPAD_R_STICK_UP,
[SDL_CONTROLLER_AXIS_TRIGGERLEFT] = INPUT_GAMEPAD_L_TRIGGER,
[SDL_CONTROLLER_AXIS_TRIGGERRIGHT] = INPUT_GAMEPAD_R_TRIGGER,
[SDL_CONTROLLER_AXIS_MAX] = INPUT_INVALID
};
void platform_exit() {
wants_to_exit = true;
}
SDL_GameController *platform_find_gamepad() {
for (int i = 0; i < SDL_NumJoysticks(); i++) {
if (SDL_IsGameController(i)) {
return SDL_GameControllerOpen(i);
}
}
return NULL;
}
void platform_pump_events() {
SDL_Event ev;
while (SDL_PollEvent(&ev)) {
// Input Keyboard
if (ev.type == SDL_KEYDOWN || ev.type == SDL_KEYUP) {
int code = ev.key.keysym.scancode;
float state = ev.type == SDL_KEYDOWN ? 1.0 : 0.0;
if (code >= SDL_SCANCODE_LCTRL && code <= SDL_SCANCODE_RALT) {
int code_internal = code - SDL_SCANCODE_LCTRL + INPUT_KEY_LCTRL;
input_set_button_state(code_internal, state);
}
else if (code > 0 && code < INPUT_KEY_MAX) {
input_set_button_state(code, state);
}
}
else if (ev.type == SDL_TEXTINPUT) {
input_textinput(ev.text.text[0]);
}
// Gamepads connect/disconnect
else if (ev.type == SDL_CONTROLLERDEVICEADDED) {
gamepad = SDL_GameControllerOpen(ev.cdevice.which);
}
else if (ev.type == SDL_CONTROLLERDEVICEREMOVED) {
if (gamepad && ev.cdevice.which == SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(gamepad))) {
SDL_GameControllerClose(gamepad);
gamepad = platform_find_gamepad();
}
}
// Input Gamepad Buttons
else if (
ev.type == SDL_CONTROLLERBUTTONDOWN ||
ev.type == SDL_CONTROLLERBUTTONUP
) {
if (ev.cbutton.button < SDL_CONTROLLER_BUTTON_MAX) {
button_t button = platform_sdl_gamepad_map[ev.cbutton.button];
if (button != INPUT_INVALID) {
float state = ev.type == SDL_CONTROLLERBUTTONDOWN ? 1.0 : 0.0;
input_set_button_state(button, state);
}
}
}
// Input Gamepad Axis
else if (ev.type == SDL_CONTROLLERAXISMOTION) {
float state = (float)ev.caxis.value / 32767.0;
if (ev.caxis.axis < SDL_CONTROLLER_AXIS_MAX) {
int code = platform_sdl_axis_map[ev.caxis.axis];
if (
code == INPUT_GAMEPAD_L_TRIGGER ||
code == INPUT_GAMEPAD_R_TRIGGER
) {
input_set_button_state(code, state);
}
else if (state > 0) {
input_set_button_state(code, 0.0);
input_set_button_state(code+1, state);
}
else {
input_set_button_state(code, -state);
input_set_button_state(code+1, 0.0);
}
}
}
// Mouse buttons
else if (
ev.type == SDL_MOUSEBUTTONDOWN ||
ev.type == SDL_MOUSEBUTTONUP
) {
button_t button = INPUT_BUTTON_NONE;
switch (ev.button.button) {
case SDL_BUTTON_LEFT: button = INPUT_MOUSE_LEFT; break;
case SDL_BUTTON_MIDDLE: button = INPUT_MOUSE_MIDDLE; break;
case SDL_BUTTON_RIGHT: button = INPUT_MOUSE_RIGHT; break;
default: break;
}
if (button != INPUT_BUTTON_NONE) {
float state = ev.type == SDL_MOUSEBUTTONDOWN ? 1.0 : 0.0;
input_set_button_state(button, state);
}
}
// Mouse wheel
else if (ev.type == SDL_MOUSEWHEEL) {
button_t button = ev.wheel.y > 0
? INPUT_MOUSE_WHEEL_UP
: INPUT_MOUSE_WHEEL_DOWN;
input_set_button_state(button, 1.0);
input_set_button_state(button, 0.0);
}
// Mouse move
else if (ev.type == SDL_MOUSEMOTION) {
input_set_mouse_pos(ev.motion.x, ev.motion.y);
}
// Window Events
if (ev.type == SDL_QUIT) {
wants_to_exit = true;
}
else if (
ev.type == SDL_WINDOWEVENT &&
(
ev.window.event == SDL_WINDOWEVENT_SIZE_CHANGED ||
ev.window.event == SDL_WINDOWEVENT_RESIZED
)
) {
system_resize(platform_screen_size());
}
}
}
double platform_now() {
uint64_t perf_counter = SDL_GetPerformanceCounter();
return (double)perf_counter / (double)perf_freq;
}
void platform_set_fullscreen(bool fullscreen) {
if (fullscreen) {
int32_t display = SDL_GetWindowDisplayIndex(window);
SDL_DisplayMode mode;
SDL_GetDesktopDisplayMode(display, &mode);
SDL_SetWindowDisplayMode(window, &mode);
SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN);
SDL_ShowCursor(SDL_DISABLE);
}
else {
SDL_SetWindowFullscreen(window, 0);
SDL_ShowCursor(SDL_ENABLE);
}
}
void platform_audio_callback(void* userdata, uint8_t* stream, int len) {
if (audio_callback) {
audio_callback((float *)stream, len/sizeof(float));
}
else {
memset(stream, 0, len);
}
}
void platform_set_audio_mix_cb(void (*cb)(float *buffer, uint32_t len)) {
audio_callback = cb;
SDL_PauseAudioDevice(audio_device, 0);
}
#if defined(RENDERER_GL) // ----------------------------------------------------
#define PLATFORM_WINDOW_FLAGS SDL_WINDOW_OPENGL
SDL_GLContext platform_gl;
void platform_video_init() {
#if defined(USE_GLES2)
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
#endif
platform_gl = SDL_GL_CreateContext(window);
SDL_GL_SetSwapInterval(1);
}
void platform_prepare_frame() {
// nothing
}
void platform_video_cleanup() {
SDL_GL_DeleteContext(platform_gl);
}
void platform_end_frame() {
SDL_GL_SwapWindow(window);
}
vec2i_t platform_screen_size() {
int width, height;
SDL_GL_GetDrawableSize(window, &width, &height);
return vec2i(width, height);
}
#elif defined(RENDERER_SOFTWARE) // ----------------------------------------------
#define PLATFORM_WINDOW_FLAGS 0
static SDL_Renderer *renderer;
static SDL_Texture *screenbuffer = NULL;
static void *screenbuffer_pixels = NULL;
static int screenbuffer_pitch;
static vec2i_t screenbuffer_size = vec2i(0, 0);
static vec2i_t screen_size = vec2i(0, 0);
void platform_video_init() {
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
}
void platform_video_cleanup() {
}
void platform_prepare_frame() {
if (screen_size.x != screenbuffer_size.x || screen_size.y != screenbuffer_size.y) {
if (screenbuffer) {
SDL_DestroyTexture(screenbuffer);
}
screenbuffer = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, screen_size.x, screen_size.y);
screenbuffer_size = screen_size;
}
SDL_LockTexture(screenbuffer, NULL, &screenbuffer_pixels, &screenbuffer_pitch);
}
void platform_end_frame() {
screenbuffer_pixels = NULL;
SDL_UnlockTexture(screenbuffer);
SDL_RenderCopy(renderer, screenbuffer, NULL, NULL);
SDL_RenderPresent(renderer);
}
rgba_t *platform_get_screenbuffer(int32_t *pitch) {
*pitch = screenbuffer_pitch;
return screenbuffer_pixels;
}
vec2i_t platform_screen_size() {
int width, height;
SDL_GetWindowSize(window, &width, &height);
// float aspect = (float)width / (float)height;
// screen_size = vec2i(240 * aspect, 240);
screen_size = vec2i(width, height);
return screen_size;
}
#else
#error "Unsupported renderer for platform SDL"
#endif
int main(int argc, char *argv[]) {
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER);
int gcdb_res = SDL_GameControllerAddMappingsFromFile("gamecontrollerdb.txt");
if (gcdb_res < 0) {
printf("Failed to load gamecontrollerdb.txt\n");
}
else {
printf("load gamecontrollerdb.txt\n");
}
audio_device = SDL_OpenAudioDevice(NULL, 0, &(SDL_AudioSpec){
.freq = 44100,
.format = AUDIO_F32,
.channels = 2,
.samples = 1024,
.callback = platform_audio_callback
}, NULL, 0);
gamepad = platform_find_gamepad();
perf_freq = SDL_GetPerformanceFrequency();
window = SDL_CreateWindow(
SYSTEM_WINDOW_NAME,
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
SYSTEM_WINDOW_WIDTH, SYSTEM_WINDOW_HEIGHT,
SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | PLATFORM_WINDOW_FLAGS | SDL_WINDOW_ALLOW_HIGHDPI
);
platform_video_init();
system_init();
while (!wants_to_exit) {
platform_pump_events();
platform_prepare_frame();
system_update();
platform_end_frame();
}
system_cleanup();
platform_video_cleanup();
SDL_CloseAudioDevice(audio_device);
SDL_Quit();
return 0;
}

@ -0,0 +1,256 @@
#include "platform.h"
#include "system.h"
#if defined(RENDERER_GL)
#ifdef __EMSCRIPTEN__
#define SOKOL_GLES2
#else
#define SOKOL_GLCORE33
#endif
#else
#error "Unsupported renderer for platform SOKOL"
#endif
#define SOKOL_IMPL
#include "libs/sokol_audio.h"
#include "libs/sokol_time.h"
#include "libs/sokol_app.h"
#include "input.h"
static const uint8_t keyboard_map[] = {
[SAPP_KEYCODE_SPACE] = INPUT_KEY_SPACE,
[SAPP_KEYCODE_APOSTROPHE] = INPUT_KEY_APOSTROPHE,
[SAPP_KEYCODE_COMMA] = INPUT_KEY_COMMA,
[SAPP_KEYCODE_MINUS] = INPUT_KEY_MINUS,
[SAPP_KEYCODE_PERIOD] = INPUT_KEY_PERIOD,
[SAPP_KEYCODE_SLASH] = INPUT_KEY_SLASH,
[SAPP_KEYCODE_0] = INPUT_KEY_0,
[SAPP_KEYCODE_1] = INPUT_KEY_1,
[SAPP_KEYCODE_2] = INPUT_KEY_2,
[SAPP_KEYCODE_3] = INPUT_KEY_3,
[SAPP_KEYCODE_4] = INPUT_KEY_4,
[SAPP_KEYCODE_5] = INPUT_KEY_5,
[SAPP_KEYCODE_6] = INPUT_KEY_6,
[SAPP_KEYCODE_7] = INPUT_KEY_7,
[SAPP_KEYCODE_8] = INPUT_KEY_8,
[SAPP_KEYCODE_9] = INPUT_KEY_9,
[SAPP_KEYCODE_SEMICOLON] = INPUT_KEY_SEMICOLON,
[SAPP_KEYCODE_EQUAL] = INPUT_KEY_EQUALS,
[SAPP_KEYCODE_A] = INPUT_KEY_A,
[SAPP_KEYCODE_B] = INPUT_KEY_B,
[SAPP_KEYCODE_C] = INPUT_KEY_C,
[SAPP_KEYCODE_D] = INPUT_KEY_D,
[SAPP_KEYCODE_E] = INPUT_KEY_E,
[SAPP_KEYCODE_F] = INPUT_KEY_F,
[SAPP_KEYCODE_G] = INPUT_KEY_G,
[SAPP_KEYCODE_H] = INPUT_KEY_H,
[SAPP_KEYCODE_I] = INPUT_KEY_I,
[SAPP_KEYCODE_J] = INPUT_KEY_J,
[SAPP_KEYCODE_K] = INPUT_KEY_K,
[SAPP_KEYCODE_L] = INPUT_KEY_L,
[SAPP_KEYCODE_M] = INPUT_KEY_M,
[SAPP_KEYCODE_N] = INPUT_KEY_N,
[SAPP_KEYCODE_O] = INPUT_KEY_O,
[SAPP_KEYCODE_P] = INPUT_KEY_P,
[SAPP_KEYCODE_Q] = INPUT_KEY_Q,
[SAPP_KEYCODE_R] = INPUT_KEY_R,
[SAPP_KEYCODE_S] = INPUT_KEY_S,
[SAPP_KEYCODE_T] = INPUT_KEY_T,
[SAPP_KEYCODE_U] = INPUT_KEY_U,
[SAPP_KEYCODE_V] = INPUT_KEY_V,
[SAPP_KEYCODE_W] = INPUT_KEY_W,
[SAPP_KEYCODE_X] = INPUT_KEY_X,
[SAPP_KEYCODE_Y] = INPUT_KEY_Y,
[SAPP_KEYCODE_Z] = INPUT_KEY_Z,
[SAPP_KEYCODE_LEFT_BRACKET] = INPUT_KEY_LEFTBRACKET,
[SAPP_KEYCODE_BACKSLASH] = INPUT_KEY_BACKSLASH,
[SAPP_KEYCODE_RIGHT_BRACKET] = INPUT_KEY_RIGHTBRACKET,
[SAPP_KEYCODE_GRAVE_ACCENT] = INPUT_KEY_TILDE,
[SAPP_KEYCODE_WORLD_1] = INPUT_INVALID, // not implemented
[SAPP_KEYCODE_WORLD_2] = INPUT_INVALID, // not implemented
[SAPP_KEYCODE_ESCAPE] = INPUT_KEY_ESCAPE,
[SAPP_KEYCODE_ENTER] = INPUT_KEY_RETURN,
[SAPP_KEYCODE_TAB] = INPUT_KEY_TAB,
[SAPP_KEYCODE_BACKSPACE] = INPUT_KEY_BACKSPACE,
[SAPP_KEYCODE_INSERT] = INPUT_KEY_INSERT,
[SAPP_KEYCODE_DELETE] = INPUT_KEY_DELETE,
[SAPP_KEYCODE_RIGHT] = INPUT_KEY_RIGHT,
[SAPP_KEYCODE_LEFT] = INPUT_KEY_LEFT,
[SAPP_KEYCODE_DOWN] = INPUT_KEY_DOWN,
[SAPP_KEYCODE_UP] = INPUT_KEY_UP,
[SAPP_KEYCODE_PAGE_UP] = INPUT_KEY_PAGEUP,
[SAPP_KEYCODE_PAGE_DOWN] = INPUT_KEY_PAGEDOWN,
[SAPP_KEYCODE_HOME] = INPUT_KEY_HOME,
[SAPP_KEYCODE_END] = INPUT_KEY_END,
[SAPP_KEYCODE_CAPS_LOCK] = INPUT_KEY_CAPSLOCK,
[SAPP_KEYCODE_SCROLL_LOCK] = INPUT_KEY_SCROLLLOCK,
[SAPP_KEYCODE_NUM_LOCK] = INPUT_KEY_NUMLOCK,
[SAPP_KEYCODE_PRINT_SCREEN] = INPUT_KEY_PRINTSCREEN,
[SAPP_KEYCODE_PAUSE] = INPUT_KEY_PAUSE,
[SAPP_KEYCODE_F1] = INPUT_KEY_F1,
[SAPP_KEYCODE_F2] = INPUT_KEY_F2,
[SAPP_KEYCODE_F3] = INPUT_KEY_F3,
[SAPP_KEYCODE_F4] = INPUT_KEY_F4,
[SAPP_KEYCODE_F5] = INPUT_KEY_F5,
[SAPP_KEYCODE_F6] = INPUT_KEY_F6,
[SAPP_KEYCODE_F7] = INPUT_KEY_F7,
[SAPP_KEYCODE_F8] = INPUT_KEY_F8,
[SAPP_KEYCODE_F9] = INPUT_KEY_F9,
[SAPP_KEYCODE_F10] = INPUT_KEY_F10,
[SAPP_KEYCODE_F11] = INPUT_KEY_F11,
[SAPP_KEYCODE_F12] = INPUT_KEY_F12,
[SAPP_KEYCODE_F13] = INPUT_INVALID, // not implemented
[SAPP_KEYCODE_F14] = INPUT_INVALID, // not implemented
[SAPP_KEYCODE_F15] = INPUT_INVALID, // not implemented
[SAPP_KEYCODE_F16] = INPUT_INVALID, // not implemented
[SAPP_KEYCODE_F17] = INPUT_INVALID, // not implemented
[SAPP_KEYCODE_F18] = INPUT_INVALID, // not implemented
[SAPP_KEYCODE_F19] = INPUT_INVALID, // not implemented
[SAPP_KEYCODE_F20] = INPUT_INVALID, // not implemented
[SAPP_KEYCODE_F21] = INPUT_INVALID, // not implemented
[SAPP_KEYCODE_F22] = INPUT_INVALID, // not implemented
[SAPP_KEYCODE_F23] = INPUT_INVALID, // not implemented
[SAPP_KEYCODE_F24] = INPUT_INVALID, // not implemented
[SAPP_KEYCODE_F25] = INPUT_INVALID, // not implemented
[SAPP_KEYCODE_KP_0] = INPUT_KEY_KP_0,
[SAPP_KEYCODE_KP_1] = INPUT_KEY_KP_1,
[SAPP_KEYCODE_KP_2] = INPUT_KEY_KP_2,
[SAPP_KEYCODE_KP_3] = INPUT_KEY_KP_3,
[SAPP_KEYCODE_KP_4] = INPUT_KEY_KP_4,
[SAPP_KEYCODE_KP_5] = INPUT_KEY_KP_5,
[SAPP_KEYCODE_KP_6] = INPUT_KEY_KP_6,
[SAPP_KEYCODE_KP_7] = INPUT_KEY_KP_7,
[SAPP_KEYCODE_KP_8] = INPUT_KEY_KP_8,
[SAPP_KEYCODE_KP_9] = INPUT_KEY_KP_9,
[SAPP_KEYCODE_KP_DECIMAL] = INPUT_KEY_KP_PERIOD,
[SAPP_KEYCODE_KP_DIVIDE] = INPUT_KEY_KP_DIVIDE,
[SAPP_KEYCODE_KP_MULTIPLY] = INPUT_KEY_KP_MULTIPLY,
[SAPP_KEYCODE_KP_SUBTRACT] = INPUT_KEY_KP_MINUS,
[SAPP_KEYCODE_KP_ADD] = INPUT_KEY_KP_PLUS,
[SAPP_KEYCODE_KP_ENTER] = INPUT_KEY_KP_ENTER,
[SAPP_KEYCODE_KP_EQUAL] = INPUT_INVALID, // not implemented
[SAPP_KEYCODE_LEFT_SHIFT] = INPUT_KEY_LSHIFT,
[SAPP_KEYCODE_LEFT_CONTROL] = INPUT_KEY_LCTRL,
[SAPP_KEYCODE_LEFT_ALT] = INPUT_KEY_LALT,
[SAPP_KEYCODE_LEFT_SUPER] = INPUT_INVALID, // not implemented
[SAPP_KEYCODE_RIGHT_SHIFT] = INPUT_KEY_RSHIFT,
[SAPP_KEYCODE_RIGHT_CONTROL] = INPUT_KEY_RCTRL,
[SAPP_KEYCODE_RIGHT_ALT] = INPUT_KEY_RALT,
[SAPP_KEYCODE_RIGHT_SUPER] = INPUT_INVALID, // not implemented
[SAPP_KEYCODE_MENU] = INPUT_INVALID, // not implemented
};
static void (*audio_callback)(float *buffer, uint32_t len) = NULL;
void platform_exit() {
sapp_quit();
}
vec2i_t platform_screen_size() {
return vec2i(sapp_width(), sapp_height());
}
double platform_now() {
return stm_sec(stm_now());
}
void platform_set_fullscreen(bool fullscreen) {
if (fullscreen == sapp_is_fullscreen()) {
return;
}
sapp_toggle_fullscreen();
sapp_show_mouse(!fullscreen);
}
void platform_handle_event(const sapp_event* ev) {
// Input Keyboard
if (ev->type == SAPP_EVENTTYPE_KEY_DOWN || ev->type == SAPP_EVENTTYPE_KEY_UP) {
float state = ev->type == SAPP_EVENTTYPE_KEY_DOWN ? 1.0 : 0.0;
if (ev->key_code > 0 && ev->key_code < sizeof(keyboard_map)) {
int code = keyboard_map[ev->key_code];
input_set_button_state(code, state);
}
}
else if (ev->type == SAPP_EVENTTYPE_CHAR) {
input_textinput(ev->char_code);
}
// Input Gamepad Axis
// TODO: not implemented by sokol_app itself
// Mouse buttons
else if (
ev->type == SAPP_EVENTTYPE_MOUSE_DOWN ||
ev->type == SAPP_EVENTTYPE_MOUSE_UP
) {
button_t button = INPUT_BUTTON_NONE;
switch (ev->mouse_button) {
case SAPP_MOUSEBUTTON_LEFT: button = INPUT_MOUSE_LEFT; break;
case SAPP_MOUSEBUTTON_MIDDLE: button = INPUT_MOUSE_MIDDLE; break;
case SAPP_MOUSEBUTTON_RIGHT: button = INPUT_MOUSE_RIGHT; break;
default: break;
}
if (button != INPUT_BUTTON_NONE) {
float state = ev->type == SAPP_EVENTTYPE_MOUSE_DOWN ? 1.0 : 0.0;
input_set_button_state(button, state);
}
}
// Mouse wheel
else if (ev->type == SAPP_EVENTTYPE_MOUSE_SCROLL) {
button_t button = ev->scroll_y > 0
? INPUT_MOUSE_WHEEL_UP
: INPUT_MOUSE_WHEEL_DOWN;
input_set_button_state(button, 1.0);
input_set_button_state(button, 0.0);
}
// Mouse move
else if (ev->type == SAPP_EVENTTYPE_MOUSE_MOVE) {
input_set_mouse_pos(ev->mouse_x, ev->mouse_y);
}
// Window Events
if (ev->type == SAPP_EVENTTYPE_RESIZED) {
system_resize(vec2i(ev->window_width, ev->window_height));
}
}
void platform_audio_callback(float* buffer, int num_frames, int num_channels) {
if (audio_callback) {
audio_callback(buffer, num_frames * num_channels);
}
else {
memset(buffer, 0, num_frames * sizeof(float));
}
}
void platform_set_audio_mix_cb(void (*cb)(float *buffer, uint32_t len)) {
audio_callback = cb;
}
sapp_desc sokol_main(int argc, char* argv[]) {
stm_setup();
saudio_setup(&(saudio_desc){
.sample_rate = 44100,
.buffer_frames = 1024,
.num_packets = 256,
.num_channels = 2,
.stream_cb = platform_audio_callback,
});
return (sapp_desc) {
.width = SYSTEM_WINDOW_WIDTH,
.height = SYSTEM_WINDOW_HEIGHT,
.init_cb = system_init,
.frame_cb = system_update,
.cleanup_cb = system_cleanup,
.event_cb = platform_handle_event,
.win32_console_attach = true
};
}

@ -0,0 +1,64 @@
#ifndef RENDER_H
#define RENDER_H
#include "types.h"
typedef enum {
RENDER_BLEND_NORMAL,
RENDER_BLEND_LIGHTER
} render_blend_mode_t;
typedef enum {
RENDER_RES_NATIVE,
RENDER_RES_240P,
RENDER_RES_480P,
} render_resolution_t;
typedef enum {
RENDER_POST_NONE,
RENDER_POST_CRT,
NUM_RENDER_POST_EFFCTS,
} render_post_effect_t;
#define RENDER_USE_MIPMAPS 1
#define RENDER_FADEOUT_NEAR 48000.0
#define RENDER_FADEOUT_FAR 64000.0
extern uint16_t RENDER_NO_TEXTURE;
void render_init(vec2i_t screen_size);
void render_cleanup();
void render_set_screen_size(vec2i_t size);
void render_set_resolution(render_resolution_t res);
void render_set_post_effect(render_post_effect_t post);
vec2i_t render_size();
void render_frame_prepare();
void render_frame_end();
void render_set_view(vec3_t pos, vec3_t angles);
void render_set_view_2d();
void render_set_model_mat(mat4_t *m);
void render_set_depth_write(bool enabled);
void render_set_depth_test(bool enabled);
void render_set_depth_offset(float offset);
void render_set_screen_position(vec2_t pos);
void render_set_blend_mode(render_blend_mode_t mode);
void render_set_cull_backface(bool enabled);
vec3_t render_transform(vec3_t pos);
void render_push_tris(tris_t tris, uint16_t texture);
void render_push_sprite(vec3_t pos, vec2i_t size, rgba_t color, uint16_t texture);
void render_push_2d(vec2i_t pos, vec2i_t size, rgba_t color, uint16_t texture);
void render_push_2d_tile(vec2i_t pos, vec2i_t uv_offset, vec2i_t uv_size, vec2i_t size, rgba_t color, uint16_t texture_index);
uint16_t render_texture_create(uint32_t width, uint32_t height, rgba_t *pixels);
vec2i_t render_texture_size(uint16_t texture_index);
void render_texture_replace_pixels(int16_t texture_index, rgba_t *pixels);
uint16_t render_textures_len();
void render_textures_reset(uint16_t len);
void render_textures_dump(const char *path);
#endif

File diff suppressed because it is too large Load Diff

@ -0,0 +1,369 @@
#include "system.h"
#include "render.h"
#include "mem.h"
#include "utils.h"
#include "platform.h"
#define NEAR_PLANE 16.0
#define FAR_PLANE (RENDER_FADEOUT_FAR)
#define TEXTURES_MAX 1024
typedef struct {
vec2i_t size;
rgba_t *pixels;
} render_texture_t;
static void line(vec2i_t p0, vec2i_t p1, rgba_t color);
static rgba_t *screen_buffer;
static int32_t screen_pitch;
static int32_t screen_ppr;
static vec2i_t screen_size;
static mat4_t view_mat = mat4_identity();
static mat4_t mvp_mat = mat4_identity();
static mat4_t projection_mat = mat4_identity();
static mat4_t sprite_mat = mat4_identity();
static render_texture_t textures[TEXTURES_MAX];
static uint32_t textures_len;
uint16_t RENDER_NO_TEXTURE;
void render_init(vec2i_t screen_size) {
render_set_screen_size(screen_size);
textures_len = 0;
rgba_t white_pixels[4] = {
rgba(128,128,128,255), rgba(128,128,128,255),
rgba(128,128,128,255), rgba(128,128,128,255)
};
RENDER_NO_TEXTURE = render_texture_create(2, 2, white_pixels);
}
void render_cleanup() {}
void render_set_screen_size(vec2i_t size) {
screen_size = size;
float aspect = (float)size.x / (float)size.y;
float fov = (73.75 / 180.0) * 3.14159265358;
float f = 1.0 / tan(fov / 2);
float nf = 1.0 / (NEAR_PLANE - FAR_PLANE);
projection_mat = mat4(
f / aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, (FAR_PLANE + NEAR_PLANE) * nf, -1,
0, 0, 2 * FAR_PLANE * NEAR_PLANE * nf, 0
);
}
void render_set_resolution(render_resolution_t res) {}
void render_set_post_effect(render_post_effect_t post) {}
vec2i_t render_size() {
return screen_size;
}
void render_frame_prepare() {
screen_buffer = platform_get_screenbuffer(&screen_pitch);
screen_ppr = screen_pitch / sizeof(rgba_t);
rgba_t color = rgba(0, 0, 0, 255);
for (uint32_t y = 0; y < screen_size.y; y++) {
for (uint32_t x = 0; x < screen_size.x; x++) {
screen_buffer[y * screen_ppr + x] = color;
}
}
}
void render_frame_end() {}
void render_set_view(vec3_t pos, vec3_t angles) {
view_mat = mat4_identity();
mat4_set_translation(&view_mat, vec3(0, 0, 0));
mat4_set_roll_pitch_yaw(&view_mat, vec3(angles.x, -angles.y + M_PI, angles.z + M_PI));
mat4_translate(&view_mat, vec3_inv(pos));
mat4_set_yaw_pitch_roll(&sprite_mat, vec3(-angles.x, angles.y - M_PI, 0));
render_set_model_mat(&mat4_identity());
}
void render_set_view_2d() {
float near = -1;
float far = 1;
float left = 0;
float right = screen_size.x;
float bottom = screen_size.y;
float top = 0;
float lr = 1 / (left - right);
float bt = 1 / (bottom - top);
float nf = 1 / (near - far);
mvp_mat = mat4(
-2 * lr, 0, 0, 0,
0, -2 * bt, 0, 0,
0, 0, 2 * nf, 0,
(left + right) * lr, (top + bottom) * bt, (far + near) * nf, 1
);
}
void render_set_model_mat(mat4_t *m) {
mat4_t vm_mat;
mat4_mul(&vm_mat, &view_mat, m);
mat4_mul(&mvp_mat, &projection_mat, &vm_mat);
}
void render_set_depth_write(bool enabled) {}
void render_set_depth_test(bool enabled) {}
void render_set_depth_offset(float offset) {}
void render_set_screen_position(vec2_t pos) {}
void render_set_blend_mode(render_blend_mode_t mode) {}
void render_set_cull_backface(bool enabled) {}
vec3_t render_transform(vec3_t pos) {
return vec3_transform(vec3_transform(pos, &view_mat), &projection_mat);
}
void render_push_tris(tris_t tris, uint16_t texture_index) {
float w2 = screen_size.x * 0.5;
float h2 = screen_size.y * 0.5;
vec3_t p0 = vec3_transform(tris.vertices[0].pos, &mvp_mat);
vec3_t p1 = vec3_transform(tris.vertices[1].pos, &mvp_mat);
vec3_t p2 = vec3_transform(tris.vertices[2].pos, &mvp_mat);
if (p0.z >= 1.0 || p1.z >= 1.0 || p2.z >= 1.0) {
return;
}
vec2i_t sc0 = vec2i(p0.x * w2 + w2, h2 - p0.y * h2);
vec2i_t sc1 = vec2i(p1.x * w2 + w2, h2 - p1.y * h2);
vec2i_t sc2 = vec2i(p2.x * w2 + w2, h2 - p2.y * h2);
rgba_t color = tris.vertices[0].color;
color.as_rgba.r = min(color.as_rgba.r * 2, 255);
color.as_rgba.g = min(color.as_rgba.g * 2, 255);
color.as_rgba.b = min(color.as_rgba.b * 2, 255);
color.as_rgba.a = clamp(color.as_rgba.a * (1.0-p0.z) * FAR_PLANE * (2.0/255.0), 0, 255);
line(sc0, sc1, color);
line(sc1, sc2, color);
line(sc2, sc0, color);
}
void render_push_sprite(vec3_t pos, vec2i_t size, rgba_t color, uint16_t texture_index) {
error_if(texture_index >= textures_len, "Invalid texture %d", texture_index);
vec3_t p0 = vec3_add(pos, vec3_transform(vec3(-size.x * 0.5, -size.y * 0.5, 0), &sprite_mat));
vec3_t p1 = vec3_add(pos, vec3_transform(vec3( size.x * 0.5, -size.y * 0.5, 0), &sprite_mat));
vec3_t p2 = vec3_add(pos, vec3_transform(vec3(-size.x * 0.5, size.y * 0.5, 0), &sprite_mat));
vec3_t p3 = vec3_add(pos, vec3_transform(vec3( size.x * 0.5, size.y * 0.5, 0), &sprite_mat));
render_texture_t *t = &textures[texture_index];
render_push_tris((tris_t){
.vertices = {
{.pos = p0, .uv = {0, 0}, .color = color},
{.pos = p1, .uv = {0 + t->size.x ,0}, .color = color},
{.pos = p2, .uv = {0, 0 + t->size.y}, .color = color},
}
}, texture_index);
render_push_tris((tris_t){
.vertices = {
{.pos = p2, .uv = {0, 0 + t->size.y}, .color = color},
{.pos = p1, .uv = {0 + t->size.x, 0}, .color = color},
{.pos = p3, .uv = {0 + t->size.x, 0 + t->size.y}, .color = color},
}
}, texture_index);
}
void render_push_2d(vec2i_t pos, vec2i_t size, rgba_t color, uint16_t texture_index) {
render_push_2d_tile(pos, vec2i(0, 0), render_texture_size(texture_index), size, color, texture_index);
}
void render_push_2d_tile(vec2i_t pos, vec2i_t uv_offset, vec2i_t uv_size, vec2i_t size, rgba_t color, uint16_t texture_index) {
error_if(texture_index >= textures_len, "Invalid texture %d", texture_index);
render_push_tris((tris_t){
.vertices = {
{.pos = {pos.x, pos.y + size.y, 0}, .uv = {uv_offset.x , uv_offset.y + uv_size.y}, .color = color},
{.pos = {pos.x + size.x, pos.y, 0}, .uv = {uv_offset.x + uv_size.x, uv_offset.y}, .color = color},
{.pos = {pos.x, pos.y, 0}, .uv = {uv_offset.x , uv_offset.y}, .color = color},
}
}, texture_index);
render_push_tris((tris_t){
.vertices = {
{.pos = {pos.x + size.x, pos.y + size.y, 0}, .uv = {uv_offset.x + uv_size.x, uv_offset.y + uv_size.y}, .color = color},
{.pos = {pos.x + size.x, pos.y, 0}, .uv = {uv_offset.x + uv_size.x, uv_offset.y}, .color = color},
{.pos = {pos.x, pos.y + size.y, 0}, .uv = {uv_offset.x , uv_offset.y + uv_size.y}, .color = color},
}
}, texture_index);
}
uint16_t render_texture_create(uint32_t width, uint32_t height, rgba_t *pixels) {
error_if(textures_len >= TEXTURES_MAX, "TEXTURES_MAX reached");
uint32_t byte_size = width * height * sizeof(rgba_t);
uint16_t texture_index = textures_len;
textures[texture_index] = (render_texture_t){{width, height}, NULL};
// textures[texture_index] = (render_texture_t){{width, height}, mem_bump(byte_size)};
// memcpy(textures[texture_index].pixels, pixels, byte_size);
textures_len++;
return texture_index;
}
vec2i_t render_texture_size(uint16_t texture_index) {
error_if(texture_index >= textures_len, "Invalid texture %d", texture_index);
return textures[texture_index].size;
}
void render_texture_replace_pixels(int16_t texture_index, rgba_t *pixels) {
error_if(texture_index >= textures_len, "Invalid texture %d", texture_index);
render_texture_t *t = &textures[texture_index];
// memcpy(t->pixels, pixels, t->size.x * t->size.y * sizeof(rgba_t));
}
uint16_t render_textures_len() {
return textures_len;
}
void render_textures_reset(uint16_t len) {
error_if(len > textures_len, "Invalid texture reset len %d >= %d", len, textures_len);
textures_len = len;
}
void render_textures_dump(const char *path) {}
// -----------------------------------------------------------------------------
static inline rgba_t color_mix(rgba_t in, rgba_t out) {
return rgba(
lerp(in.as_rgba.r, out.as_rgba.r, out.as_rgba.a/255.0),
lerp(in.as_rgba.g, out.as_rgba.g, out.as_rgba.a/255.0),
lerp(in.as_rgba.b, out.as_rgba.b, out.as_rgba.a/255.0),
1
);
}
typedef enum {
CLIP_INSIDE = 0,
CLIP_LEFT = (1<<0),
CLIP_RIGHT = (1<<1),
CLIP_BOTTOM = (1<<2),
CLIP_TOP = (1<<3),
} clip_code_t;
static inline clip_code_t clip_code(vec2i_t p) {
clip_code_t cc = CLIP_INSIDE;
if (p.x < 0) {
flags_add(cc, CLIP_LEFT);
}
else if (p.x >= screen_size.x) {
flags_add(cc, CLIP_RIGHT);
}
if (p.y < 0) {
flags_add(cc, CLIP_BOTTOM);
}
else if (p.y >= screen_size.y) {
flags_add(cc, CLIP_TOP);
}
return cc;
}
static void line(vec2i_t p0, vec2i_t p1, rgba_t color) {
// Cohen Sutherland Line Clipping
clip_code_t cc0 = clip_code(p0);
clip_code_t cc1 = clip_code(p1);
bool accept = false;
vec2i_t ss = vec2i(screen_size.x-1, screen_size.y-1);
while (true) {
if (!(cc0 | cc1)) {
accept = true;
break;
}
else if (cc0 & cc1) {
break;
}
else {
vec2i_t r = p0;
clip_code_t cc_out = cc0 ? cc0 : cc1;
if (flags_is(cc_out, CLIP_TOP)) {
r.x = p0.x + (p1.x - p0.x) * (ss.y - p0.y) / (p1.y - p0.y);
r.y = ss.y;
}
else if (flags_is(cc_out, CLIP_BOTTOM)) {
r.x = p0.x + (p1.x - p0.x) * (-p0.y) / (p1.y - p0.y);
r.y = 0;
}
else if (flags_is(cc_out, CLIP_RIGHT)) {
r.y = p0.y + (p1.y - p0.y) * (ss.x - p0.x) / (p1.x - p0.x);
r.x = ss.x;
}
else if (flags_is(cc_out, CLIP_LEFT)) {
r.y = p0.y + (p1.y - p0.y) * (-p0.x) / (p1.x - p0.x);
r.x = 0;
}
if (cc_out == cc0) {
p0.x = r.x;
p0.y = r.y;
cc0 = clip_code(p0);
}
else {
p1.x = r.x;
p1.y = r.y;
cc1 = clip_code(p1);
}
}
}
if (!accept) {
return;
}
// Bresenham's line algorithm
bool steep = false;
if (abs(p0.x - p1.x) < abs(p0.y - p1.y)) {
swap(p0.x, p0.y);
swap(p1.x, p1.y);
steep = true;
}
if (p0.x > p1.x) {
swap(p0.x, p1.x);
swap(p0.y, p1.y);
}
int32_t dx = p1.x - p0.x;
int32_t dy = p1.y - p0.y;
int32_t derror2 = abs(dy) * 2;
int32_t error2 = 0;
int32_t y = p0.y;
int32_t ydir = (p1.y > p0.y ? 1 : -1);
if (steep) {
for (int32_t x = p0.x; x <= p1.x; x++) {
screen_buffer[x * screen_ppr + y] = color_mix(screen_buffer[x * screen_ppr + y], color);
error2 += derror2;
if (error2 > dx) {
y += ydir;
error2 -= dx * 2;
}
}
}
else {
for (int32_t x = p0.x; x <= p1.x; x++) {
screen_buffer[y * screen_ppr + x] = color_mix(screen_buffer[y * screen_ppr + x], color);
error2 += derror2;
if (error2 > dx) {
y += ydir;
error2 -= dx * 2;
}
}
}
}

@ -0,0 +1,81 @@
#include "system.h"
#include "input.h"
#include "render.h"
#include "platform.h"
#include "mem.h"
#include "utils.h"
#include "wipeout/game.h"
static double time_real;
static double time_scaled;
static double time_scale = 1.0;
static double tick_last;
static double cycle_time = 0;
void system_init() {
time_real = platform_now();
input_init();
render_init(platform_screen_size());
game_init();
}
void system_cleanup() {
render_cleanup();
input_cleanup();
}
void system_exit() {
platform_exit();
}
void system_update() {
double time_real_now = platform_now();
double real_delta = time_real_now - time_real;
time_real = time_real_now;
tick_last = min(real_delta, 0.1) * time_scale;
time_scaled += tick_last;
// FIXME: come up with a better way to wrap the cycle_time, so that it
// doesn't lose precission, but also doesn't jump upon reset.
cycle_time = time_scaled;
if (cycle_time > 3600 * M_PI) {
cycle_time -= 3600 * M_PI;
}
render_frame_prepare();
game_update();
render_frame_end();
input_clear();
mem_temp_check();
}
void system_reset_cycle_time() {
cycle_time = 0;
}
void system_resize(vec2i_t size) {
render_set_screen_size(size);
}
double system_time_scale_get() {
return time_scale;
}
void system_time_scale_set(double scale) {
time_scale = scale;
}
double system_tick() {
return tick_last;
}
double system_time() {
return time_scaled;
}
double system_cycle_time() {
return cycle_time;
}

@ -0,0 +1,23 @@
#ifndef SYSTEM_H
#define SYSTEM_H
#include "types.h"
#define SYSTEM_WINDOW_NAME "wipEout"
#define SYSTEM_WINDOW_WIDTH 1280
#define SYSTEM_WINDOW_HEIGHT 720
void system_init();
void system_update();
void system_cleanup();
void system_exit();
void system_resize(vec2i_t size);
double system_time();
double system_tick();
double system_cycle_time();
void system_reset_cycle_time();
double system_time_scale_get();
void system_time_scale_set(double ts);
#endif

@ -0,0 +1,116 @@
#include <math.h>
#include "types.h"
#include "utils.h"
vec3_t vec3_wrap_angle(vec3_t a) {
return vec3(wrap_angle(a.x), wrap_angle(a.y), wrap_angle(a.z));
}
float vec3_angle(vec3_t a, vec3_t b) {
float magnitude = sqrt(
(a.x * a.x + a.y * a.y + a.z * a.z) *
(b.x * b.x + b.y * b.y + b.z * b.z)
);
float cosine = (magnitude == 0)
? 1
: vec3_dot(a, b) / magnitude;
return acos(clamp(cosine, -1, 1));
}
vec3_t vec3_transform(vec3_t a, mat4_t *mat) {
float w = mat->m[3] * a.x + mat->m[7] * a.y + mat->m[11] * a.z + mat->m[15];
if (w == 0) {
w = 1;
}
return vec3(
(mat->m[0] * a.x + mat->m[4] * a.y + mat->m[ 8] * a.z + mat->m[12]) / w,
(mat->m[1] * a.x + mat->m[5] * a.y + mat->m[ 9] * a.z + mat->m[13]) / w,
(mat->m[2] * a.x + mat->m[6] * a.y + mat->m[10] * a.z + mat->m[14]) / w
);
}
vec3_t vec3_project_to_ray(vec3_t p, vec3_t r0, vec3_t r1) {
vec3_t ray = vec3_normalize(vec3_sub(r1, r0));
float dp = vec3_dot(vec3_sub(p, r0), ray);
return vec3_add(r0, vec3_mulf(ray, dp));
}
float vec3_distance_to_plane(vec3_t p, vec3_t plane_pos, vec3_t plane_normal) {
float dot_product = vec3_dot(vec3_sub(plane_pos, p), plane_normal);
float norm_dot_product = vec3_dot(vec3_mulf(plane_normal, -1), plane_normal);
return dot_product / norm_dot_product;
}
vec3_t vec3_reflect(vec3_t incidence, vec3_t normal, float f) {
return vec3_add(incidence, vec3_mulf(normal, vec3_dot(normal, vec3_mulf(incidence, -1)) * f));
}
void mat4_set_translation(mat4_t *mat, vec3_t pos) {
mat->cols[3][0] = pos.x;
mat->cols[3][1] = pos.y;
mat->cols[3][2] = pos.z;
}
void mat4_set_yaw_pitch_roll(mat4_t *mat, vec3_t rot) {
float sx = sin( rot.x);
float sy = sin(-rot.y);
float sz = sin(-rot.z);
float cx = cos( rot.x);
float cy = cos(-rot.y);
float cz = cos(-rot.z);
mat->cols[0][0] = cy * cz + sx * sy * sz;
mat->cols[1][0] = cz * sx * sy - cy * sz;
mat->cols[2][0] = cx * sy;
mat->cols[0][1] = cx * sz;
mat->cols[1][1] = cx * cz;
mat->cols[2][1] = -sx;
mat->cols[0][2] = -cz * sy + cy * sx * sz;
mat->cols[1][2] = cy * cz * sx + sy * sz;
mat->cols[2][2] = cx * cy;
}
void mat4_set_roll_pitch_yaw(mat4_t *mat, vec3_t rot) {
float sx = sin( rot.x);
float sy = sin(-rot.y);
float sz = sin(-rot.z);
float cx = cos( rot.x);
float cy = cos(-rot.y);
float cz = cos(-rot.z);
mat->cols[0][0] = cy * cz - sx * sy * sz;
mat->cols[1][0] = -cx * sz;
mat->cols[2][0] = cz * sy + cy * sx * sz;
mat->cols[0][1] = cz * sx * sy + cy * sz;
mat->cols[1][1] = cx *cz;
mat->cols[2][1] = -cy * cz * sx + sy * sz;
mat->cols[0][2] = -cx * sy;
mat->cols[1][2] = sx;
mat->cols[2][2] = cx * cy;
}
void mat4_translate(mat4_t *mat, vec3_t translation) {
mat->m[12] = mat->m[0] * translation.x + mat->m[4] * translation.y + mat->m[8] * translation.z + mat->m[12];
mat->m[13] = mat->m[1] * translation.x + mat->m[5] * translation.y + mat->m[9] * translation.z + mat->m[13];
mat->m[14] = mat->m[2] * translation.x + mat->m[6] * translation.y + mat->m[10] * translation.z + mat->m[14];
mat->m[15] = mat->m[3] * translation.x + mat->m[7] * translation.y + mat->m[11] * translation.z + mat->m[15];
}
void mat4_mul(mat4_t *res, mat4_t *a, mat4_t *b) {
res->m[ 0] = b->m[ 0] * a->m[0] + b->m[ 1] * a->m[4] + b->m[ 2] * a->m[ 8] + b->m[ 3] * a->m[12];
res->m[ 1] = b->m[ 0] * a->m[1] + b->m[ 1] * a->m[5] + b->m[ 2] * a->m[ 9] + b->m[ 3] * a->m[13];
res->m[ 2] = b->m[ 0] * a->m[2] + b->m[ 1] * a->m[6] + b->m[ 2] * a->m[10] + b->m[ 3] * a->m[14];
res->m[ 3] = b->m[ 0] * a->m[3] + b->m[ 1] * a->m[7] + b->m[ 2] * a->m[11] + b->m[ 3] * a->m[15];
res->m[ 4] = b->m[ 4] * a->m[0] + b->m[ 5] * a->m[4] + b->m[ 6] * a->m[ 8] + b->m[ 7] * a->m[12];
res->m[ 5] = b->m[ 4] * a->m[1] + b->m[ 5] * a->m[5] + b->m[ 6] * a->m[ 9] + b->m[ 7] * a->m[13];
res->m[ 6] = b->m[ 4] * a->m[2] + b->m[ 5] * a->m[6] + b->m[ 6] * a->m[10] + b->m[ 7] * a->m[14];
res->m[ 7] = b->m[ 4] * a->m[3] + b->m[ 5] * a->m[7] + b->m[ 6] * a->m[11] + b->m[ 7] * a->m[15];
res->m[ 8] = b->m[ 8] * a->m[0] + b->m[ 9] * a->m[4] + b->m[10] * a->m[ 8] + b->m[11] * a->m[12];
res->m[ 9] = b->m[ 8] * a->m[1] + b->m[ 9] * a->m[5] + b->m[10] * a->m[ 9] + b->m[11] * a->m[13];
res->m[10] = b->m[ 8] * a->m[2] + b->m[ 9] * a->m[6] + b->m[10] * a->m[10] + b->m[11] * a->m[14];
res->m[11] = b->m[ 8] * a->m[3] + b->m[ 9] * a->m[7] + b->m[10] * a->m[11] + b->m[11] * a->m[15];
res->m[12] = b->m[12] * a->m[0] + b->m[13] * a->m[4] + b->m[14] * a->m[ 8] + b->m[15] * a->m[12];
res->m[13] = b->m[12] * a->m[1] + b->m[13] * a->m[5] + b->m[14] * a->m[ 9] + b->m[15] * a->m[13];
res->m[14] = b->m[12] * a->m[2] + b->m[13] * a->m[6] + b->m[14] * a->m[10] + b->m[15] * a->m[14];
res->m[15] = b->m[12] * a->m[3] + b->m[13] * a->m[7] + b->m[14] * a->m[11] + b->m[15] * a->m[15];
}

@ -0,0 +1,184 @@
#ifndef TYPES_H
#define TYPES_H
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
typedef union rgba_t {
struct {
uint8_t r, g, b, a;
} as_rgba;
uint8_t as_components[4];
uint32_t as_uint32;
} rgba_t;
typedef struct {
float x, y;
} vec2_t;
typedef struct {
int32_t x, y;
} vec2i_t;
typedef struct {
float x, y, z;
} vec3_t;
typedef union {
float m[16];
float cols[4][4];
} mat4_t;
typedef struct {
vec3_t pos;
vec2_t uv;
rgba_t color;
} vertex_t;
typedef struct {
vertex_t vertices[3];
} tris_t;
#define rgba(R, G, B, A) ((rgba_t){.as_rgba = {.r = R, .g = G, .b = B, .a = A}})
#define vec2(X, Y) ((vec2_t){.x = X, .y = Y})
#define vec3(X, Y, Z) ((vec3_t){.x = X, .y = Y, .z = Z})
#define vec2i(X, Y) ((vec2i_t){.x = X, .y = Y})
#define mat4(m0,m1,m2,m3,m4,m5,m6,m7,m8,m9,m10,m11,m12,m13,m14,m15) \
(mat4_t){.m = { \
m0, m1, m2, m3, \
m4, m5, m6, m7, \
m8, m9, m10, m11, \
m12, m13, m14, m15 \
}}
#define mat4_identity() mat4( \
1, 0, 0, 0, \
0, 1, 0, 0, \
0, 0, 1, 0, \
0, 0, 0, 1 \
)
static inline vec2_t vec2_mulf(vec2_t a, float f) {
return vec2(
a.x * f,
a.y * f
);
}
static inline vec2i_t vec2i_mulf(vec2i_t a, float f) {
return vec2i(
a.x * f,
a.y * f
);
}
static inline vec3_t vec3_add(vec3_t a, vec3_t b) {
return vec3(
a.x + b.x,
a.y + b.y,
a.z + b.z
);
}
static inline vec3_t vec3_sub(vec3_t a, vec3_t b) {
return vec3(
a.x - b.x,
a.y - b.y,
a.z - b.z
);
}
static inline vec3_t vec3_mul(vec3_t a, vec3_t b) {
return vec3(
a.x * b.x,
a.y * b.y,
a.z * b.z
);
}
static inline vec3_t vec3_mulf(vec3_t a, float f) {
return vec3(
a.x * f,
a.y * f,
a.z * f
);
}
static inline vec3_t vec3_inv(vec3_t a) {
return vec3(-a.x, -a.y, -a.z);
}
static inline vec3_t vec3_divf(vec3_t a, float f) {
return vec3(
a.x / f,
a.y / f,
a.z / f
);
}
static inline float vec3_len(vec3_t a) {
return sqrt(a.x * a.x + a.y * a.y + a.z * a.z);
}
static inline vec3_t vec3_cross(vec3_t a, vec3_t b) {
return vec3(
a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x
);
}
static inline float vec3_dot(vec3_t a, vec3_t b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
static inline vec3_t vec3_lerp(vec3_t a, vec3_t b, float t) {
return vec3(
a.x + t * (b.x - a.x),
a.y + t * (b.y - a.y),
a.z + t * (b.z - a.z)
);
}
static inline vec3_t vec3_normalize(vec3_t a) {
float length = vec3_len(a);
return vec3(
a.x / length,
a.y / length,
a.z / length
);
}
static inline float wrap_angle(float a) {
a = fmod(a + M_PI, M_PI * 2);
if (a < 0) {
a += M_PI * 2;
}
return a - M_PI;
}
float vec3_angle(vec3_t a, vec3_t b);
vec3_t vec3_wrap_angle(vec3_t a);
vec3_t vec3_normalize(vec3_t a);
vec3_t vec3_project_to_ray(vec3_t p, vec3_t r0, vec3_t r1);
float vec3_distance_to_plane(vec3_t p, vec3_t plane_pos, vec3_t plane_normal);
vec3_t vec3_reflect(vec3_t incidence, vec3_t normal, float f);
float wrap_angle(float a);
vec3_t vec3_transform(vec3_t a, mat4_t *mat);
void mat4_set_translation(mat4_t *mat, vec3_t pos);
void mat4_set_yaw_pitch_roll(mat4_t *m, vec3_t rot);
void mat4_set_roll_pitch_yaw(mat4_t *mat, vec3_t rot);
void mat4_translate(mat4_t *mat, vec3_t translation);
void mat4_mul(mat4_t *res, mat4_t *a, mat4_t *b);
#endif

@ -0,0 +1,68 @@
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include "utils.h"
#include "mem.h"
char temp_path[64];
char *get_path(const char *dir, const char *file) {
strcpy(temp_path, dir);
strcpy(temp_path + strlen(dir), file);
return temp_path;
}
bool file_exists(char *path) {
struct stat s;
return (stat(path, &s) == 0);
}
uint8_t *file_load(char *path, uint32_t *bytes_read) {
FILE *f = fopen(path, "rb");
error_if(!f, "Could not open file for reading: %s", path);
fseek(f, 0, SEEK_END);
int32_t size = ftell(f);
if (size <= 0) {
fclose(f);
return NULL;
}
fseek(f, 0, SEEK_SET);
uint8_t *bytes = mem_temp_alloc(size);
if (!bytes) {
fclose(f);
return NULL;
}
*bytes_read = fread(bytes, 1, size, f);
fclose(f);
error_if(*bytes_read != size, "Could not read file: %s", path);
return bytes;
}
uint32_t file_store(char *path, void *bytes, int32_t len) {
FILE *f = fopen(path, "wb");
error_if(!f, "Could not open file for writing: %s", path);
if (fwrite(bytes, 1, len, f) != len) {
die("Could not write file file %s", path);
}
fclose(f);
return len;
}
bool str_starts_with(const char *haystack, const char *needle) {
return (strncmp(haystack, needle, strlen(needle)) == 0);
}
float rand_float(float min, float max) {
return min + ((float)rand() / (float)RAND_MAX) * (max - min);
}
int32_t rand_int(int32_t min, int32_t max) {
return min + rand() % (max - min);
}

@ -0,0 +1,139 @@
#ifndef UTILS_H
#define UTILS_H
#include <string.h>
#include "types.h"
#if !defined(offsetof)
#define offsetof(TYPE, ELEMENT) ((size_t)&(((TYPE *)0)->ELEMENT))
#endif
#define member_size(type, member) sizeof(((type *)0)->member)
#define max(a,b) ({ \
__typeof__ (a) _a = (a); \
__typeof__ (b) _b = (b); \
_a > _b ? _a : _b; \
})
#define min(a,b) ({ \
__typeof__ (a) _a = (a); \
__typeof__ (b) _b = (b); \
_a < _b ? _a : _b; \
})
#define swap(a, b) ({ \
__typeof__(a) tmp = a; a = b; b = tmp; \
})
#define clamp(v, min, max) ({ \
__typeof__(v) _v = v, _min = min, _max = max; \
_v > _max ? _max : _v < _min ? _min : _v; \
})
#define scale(v, in_min, in_max, out_min, out_max) ({ \
__typeof__(v) _in_min = in_min, _out_min = out_min; \
_out_min + ((out_max) - _out_min) * (((v) - _in_min) / ((in_max) - _in_min)); \
})
#define lerp(a, b, t) ({ \
__typeof__(a) _a = a; \
_a + ((b) - _a) * (t); \
})
#define len(A) (sizeof(A) / sizeof(A[0]))
#define clear(A) memset(A, 0, sizeof(A))
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
#define die(...) \
printf("Abort at " TOSTRING(__FILE__) " line " TOSTRING(__LINE__) ": " __VA_ARGS__); \
printf("\n"); \
exit(1)
#define error_if(TEST, ...) \
if (TEST) { \
die(__VA_ARGS__); \
}
#define flags_add(FLAGS, F) (FLAGS |= (F))
#define flags_rm(FLAGS, F) (FLAGS &= ~(F))
#define flags_is(FLAGS, F) ((FLAGS & (F)) == (F))
#define flags_any(FLAGS, F) (FLAGS & (F))
#define flags_not(FLAGS, F) ((FLAGS & (F)) != (F))
#define flags_none(FLAGS, F) ((FLAGS & (F)) == 0)
#define flags_set(FLAGS, F) (FLAGS = (F))
char *get_path(const char *dir, const char *file);
bool str_starts_with(const char *haystack, const char *needle);
float rand_float(float min, float max);
int32_t rand_int(int32_t min, int32_t max);
bool file_exists(char *path);
uint8_t *file_load(char *path, uint32_t *bytes_read);
uint32_t file_store(char *path, void *bytes, int32_t len);
#define sort(LIST, LEN, COMPARE_FUNC) \
for (uint32_t sort_i = 1, sort_j; sort_i < (LEN); sort_i++) { \
sort_j = sort_i; \
__typeof__((LIST)[0]) sort_temp = (LIST)[sort_j]; \
while (sort_j > 0 && COMPARE_FUNC(&(LIST)[sort_j-1], &sort_temp)) { \
(LIST)[sort_j] = (LIST)[sort_j-1]; \
sort_j--; \
} \
(LIST)[sort_j] = sort_temp; \
}
#define shuffle(LIST, LEN) \
for (int i = (LEN) - 1; i > 0; i--) { \
int j = rand_int(0, i+1); \
swap((LIST)[i], (LIST)[j]); \
}
static inline uint8_t get_u8(uint8_t *bytes, uint32_t *p) {
return bytes[(*p)++];
}
static inline uint16_t get_u16(uint8_t *bytes, uint32_t *p) {
uint16_t v = 0;
v |= bytes[(*p)++] << 8;
v |= bytes[(*p)++] << 0;
return v;
}
static inline uint32_t get_u32(uint8_t *bytes, uint32_t *p) {
uint32_t v = 0;
v |= bytes[(*p)++] << 24;
v |= bytes[(*p)++] << 16;
v |= bytes[(*p)++] << 8;
v |= bytes[(*p)++] << 0;
return v;
}
static inline uint16_t get_u16_le(uint8_t *bytes, uint32_t *p) {
uint16_t v = 0;
v |= bytes[(*p)++] << 0;
v |= bytes[(*p)++] << 8;
return v;
}
static inline uint32_t get_u32_le(uint8_t *bytes, uint32_t *p) {
uint32_t v = 0;
v |= bytes[(*p)++] << 0;
v |= bytes[(*p)++] << 8;
v |= bytes[(*p)++] << 16;
v |= bytes[(*p)++] << 24;
return v;
}
#define get_i8(BYTES, P) ((int8_t)get_u8(BYTES, P))
#define get_i16(BYTES, P) ((int16_t)get_u16(BYTES, P))
#define get_i16_le(BYTES, P) ((int16_t)get_u16_le(BYTES, P))
#define get_i32(BYTES, P) ((int32_t)get_u32(BYTES, P))
#define get_i32_le(BYTES, P) ((int32_t)get_u32_le(BYTES, P))
#endif

@ -0,0 +1,238 @@
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>wipEout</title>
<style>
html, body {
color: #ccc;
font-family: Monospace;
font-size: 13px;
background-color: #111;
margin: 0px;
}
a {
color: #ffc603;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* the canvas *must not* have any border or padding, or mouse coords will be wrong */
canvas {
border: 0px none;
background-color: black;
width: 100%;
height: 100%;
aspect-ratio: 16 / 9;
}
#head {
left: 0;
right: 0;
top: 0;
padding: 4px 16px 6px 16px;
xheight: 24px;
background-color: #222;
display: flex;
justify-content: space-between;
}
@keyframes page-loader {
0% {transform: rotate(0deg); }
100% { transform: rotate(360deg);}
}
.container {
position: relative;
}
.info-overlay {
text-align: center;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
width: 200px;
height: 200px;
display: none;
}
.fullscreen {
margin: 0 8px;
}
#spinner {
content: "";
border-radius: 50%;
width: 48px;
height: 48px;
position: absolute;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
border-top: 2px solid #222;
border-right: 2px solid #222;
border-bottom: 2px solid #222;
border-left: 2px solid #ffc603;
transform: translateZ(0);
animation: page-loader 1.1s infinite linear;
}
h1 {
color: #fff;
display: inline-block;
margin: 0;
padding: 0;
font-size: 12px;
}
.key {
margin-right: 16px;
}
kbd {
background-color: #eee;
border-radius: 3px;
border: 1px solid #b4b4b4;
box-shadow:
0 1px 1px rgba(0, 0, 0, 0.2),
0 2px 0 0 rgba(255, 255, 255, 0.7) inset;
color: #333;
display: inline-block;
font-size: 0.85em;
font-weight: 700;
line-height: 1;
padding: 1px 2px;
white-space: nowrap;
}
</style>
</head>
<body>
<div id="head">
<div>
<h1>wipEout</h1>
<a href="#" id="fullscreen">fullscreen</a>
<span class="key"><kbd></kbd><kbd></kbd><kbd></kbd><kbd></kbd> steering</span>
<span class="key"><kbd>X</kbd> thrust</span>
<span class="key"><kbd>Z</kbd> shoot</span>
<span class="key"><kbd>C</kbd> brake left</span>
<span class="key"><kbd>V</kbd> brake right</span>
<span class="key"><kbd>A</kbd> view</span>
</div>
<div>
Read:
<a href="https://phoboslab.org/log/2023/08/rewriting-wipeout">
Rewriting wipEout
</a>
</div>
</div>
<div id="game" class="container">
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1></canvas>
<div class="info-overlay" id="loading">
<div id="spinner"></div>
<div id="status">Downloading...</div>
<div>
<progress value="0" max="100" id="progress" hidden=1></progress>
</div>
</div>
<div class="info-overlay" id="select-version">
<p>
<a href="#" id="load-full-version">FULL VERSION</a><br/>
the complete game ~144mb
</p>
<p>
<a href="#" id="load-minimal-version">MINIMAL VERSION</a><br/>
no intro, no music ~11mb
</p>
</div>
</div>
</div>
<script type='text/javascript'>
var loadScript = (ev, src) => {
ev.preventDefault();
// Hide select-version, show loader
document.getElementById('select-version').style.display = 'none';
document.getElementById('loading').style.display = 'block';
// Load the requested script
var s = document.createElement('script');
s.setAttribute('src', src);
document.head.appendChild(s);
// Attemp to unlock Audio :(
var audioCtx = new AudioContext();
audioCtx.resume();
return false;
};
var requestFullscreen = (ev) => {
ev.preventDefault();
document.getElementById('game').requestFullscreen();
};
document.getElementById('select-version').style.display = 'block';
document.getElementById('fullscreen').addEventListener('click', (ev) => requestFullscreen(ev, 'wipeout.js'))
document.getElementById('load-full-version').addEventListener('click', (ev) => loadScript(ev, 'wipeout.js'));
document.getElementById('load-minimal-version').addEventListener('click', (ev) => loadScript(ev, 'wipeout-minimal.js'));
var statusElement = document.getElementById('status');
var progressElement = document.getElementById('progress');
var spinnerElement = document.getElementById('spinner');
var Module = {
preRun: [],
postRun: [],
print: function(text) {
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
console.log(text);
},
canvas: document.getElementById('canvas'),
setStatus: (text) => {
if (!Module.setStatus.last) {
Module.setStatus.last = { time: Date.now(), text: '' };
}
if (text === Module.setStatus.last.text) {
return;
}
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
var now = Date.now();
if (m && now - Module.setStatus.last.time < 30) {
return; // if this is a progress update, skip it if too soon
}
Module.setStatus.last.time = now;
Module.setStatus.last.text = text;
if (m) {
text = m[1];
progressElement.value = parseInt(m[2])*100;
progressElement.max = parseInt(m[4])*100;
progressElement.hidden = false;
spinnerElement.hidden = false;
} else {
progressElement.value = null;
progressElement.max = null;
progressElement.hidden = true;
if (!text) {
spinnerElement.hidden = true;
document.getElementById('loading').style.display = 'none';
}
}
statusElement.innerHTML = text;
},
totalDependencies: 0,
monitorRunDependencies: (left) => {
Module.totalDependencies = Math.max(Module.totalDependencies, left);
Module.setStatus(left ? 'preparing... (' + (Module.totalDependencies-left) + '/' + Module.totalDependencies + ')' : 'all downloads complete.');
}
};
Module.setStatus('downloading...');
window.onerror = () => {
Module.setStatus('Exception thrown, see JavaScript console');
document.getElementById('loading').style.display = 'block';
spinnerElement.style.display = 'none';
Module.setStatus = (text) => {
if (text) {
console.error('[post-exception status] ' + text);
}
};
};
</script>
</body>
</html>

@ -0,0 +1,167 @@
#include "../mem.h"
#include "../utils.h"
#include "../types.h"
#include "../render.h"
#include "../system.h"
#include "object.h"
#include "track.h"
#include "ship.h"
#include "weapon.h"
#include "droid.h"
#include "camera.h"
void camera_init(camera_t *camera, section_t *section) {
camera->section = section;
for (int i = 0; i < 10; i++) {
camera->section = camera->section->next;
}
camera->position = camera->section->center;
camera->velocity = vec3(0, 0, 0);
camera->angle = vec3(0, 0, 0);
camera->angular_velocity = vec3(0, 0, 0);
camera->mat = mat4_identity();
camera->has_initial_section = false;
}
void camera_update(camera_t *camera, ship_t *ship, droid_t *droid) {
camera->last_position = camera->position;
(camera->update_func)(camera, ship, droid);
camera->real_velocity = vec3_mulf(vec3_sub(camera->position, camera->last_position), 1.0/system_tick());
}
void camera_update_race_external(camera_t *camera, ship_t *ship, droid_t *droid) {
vec3_t pos = vec3_sub(ship->position, vec3_mulf(ship->dir_forward, 1024));
pos.y -= 200;
camera->section = track_nearest_section(pos, camera->section, NULL);
section_t *next = camera->section->next;
vec3_t target = vec3_project_to_ray(pos, next->center, camera->section->center);
vec3_t diff_from_center = vec3_sub(pos, target);
vec3_t acc = diff_from_center;
acc.y += vec3_len(diff_from_center) * 0.5;
camera->velocity = vec3_sub(camera->velocity, vec3_mulf(acc, 0.015625 * 30 * system_tick()));
camera->velocity = vec3_sub(camera->velocity, vec3_mulf(camera->velocity, 0.125 * 30 * system_tick()));
pos = vec3_add(pos, camera->velocity);
camera->position = pos;
camera->angle = vec3(ship->angle.x, ship->angle.y, 0);
}
void camera_update_race_internal(camera_t *camera, ship_t *ship, droid_t *droid) {
camera->section = ship->section;
camera->position = ship_cockpit(ship);
camera->angle = vec3(ship->angle.x, ship->angle.y, ship->angle.z);
}
void camera_update_race_intro(camera_t *camera, ship_t *ship, droid_t *droid) {
// Set to final position
vec3_t pos = vec3_sub(ship->position, vec3_mulf(ship->dir_forward, 0.25 * 4096));
pos.x += sin(( (ship->update_timer - UPDATE_TIME_RACE_VIEW) * 30 * 3.0 * M_PI * 2) / 4096.0) * 4096;
pos.y -= (2 * (ship->update_timer - UPDATE_TIME_RACE_VIEW) * 30) + 200;
pos.z += sin(( (ship->update_timer - UPDATE_TIME_RACE_VIEW) * 30 * 3.0 * M_PI * 2) / 4096.0) * 4096;
if (!camera->has_initial_section) {
camera->section = ship->section;
camera->has_initial_section = true;
}
else {
camera->section = track_nearest_section(pos, camera->section, NULL);
}
camera->position = pos;
camera->angle.z = 0;
camera->angle.x = ship->angle.x * 0.5;
vec3_t target = vec3_sub(ship->position, pos);
camera->angle.y = -atan2(target.x, target.z);
if (ship->update_timer <= UPDATE_TIME_RACE_VIEW) {
flags_add(ship->flags, SHIP_VIEW_INTERNAL);
camera->update_func = camera_update_race_internal;
}
}
void camera_update_attract_circle(camera_t *camera, ship_t *ship, droid_t *droid) {
camera->update_timer -= system_tick();
if (camera->update_timer <= 0) {
camera->update_func = camera_update_attract_random;
}
// FIXME: not exactly sure what I'm doing here. The PSX version behaves
// differently.
camera->section = ship->section;
camera->position.x = ship->position.x + sin(ship->angle.y) * 512;
camera->position.y = ship->position.y + ((ship->angle.x * 512 / (M_PI * 2)) - 200);
camera->position.z = ship->position.z - cos(ship->angle.y) * 512;
camera->position.x += sin(camera->update_timer * 0.25) * 512;
camera->position.y -= 400;
camera->position.z += cos(camera->update_timer * 0.25) * 512;
camera->position = vec3_sub(camera->position, vec3_mulf(ship->dir_up, 256));
vec3_t target = vec3_sub(ship->position, camera->position);
float height = sqrt(target.x * target.x + target.z * target.z);
camera->angle.x = -atan2(target.y, height);
camera->angle.y = -atan2(target.x, target.z);
}
void camera_update_rescue(camera_t *camera, ship_t *ship, droid_t *droid) {
camera->position = vec3_add(camera->section->center, vec3(300, -1500, 300));
vec3_t target = vec3_sub(droid->position, camera->position);
float height = sqrt(target.x * target.x + target.z * target.z);
camera->angle.x = -atan2(target.y, height);
camera->angle.y = -atan2(target.x, target.z);
}
void camera_update_attract_internal(camera_t *camera, ship_t *ship, droid_t *droid) {
camera->update_timer -= system_tick();
if (camera->update_timer <= 0) {
camera->update_func = camera_update_attract_random;
}
camera->section = ship->section;
camera->position = ship_cockpit(ship);
camera->angle = vec3(ship->angle.x, ship->angle.y, 0); // No roll
}
void camera_update_static_follow(camera_t *camera, ship_t *ship, droid_t *droid) {
camera->update_timer -= system_tick();
if (camera->update_timer <= 0) {
camera->update_func = camera_update_attract_random;
}
vec3_t target = vec3_sub(ship->position, camera->position);
float height = sqrt(target.x * target.x + target.z * target.z);
camera->angle.x = -atan2(target.y, height);
camera->angle.y = -atan2(target.x, target.z);
}
void camera_update_attract_random(camera_t *camera, ship_t *ship, droid_t *droid) {
flags_rm(ship->flags, SHIP_VIEW_INTERNAL);
if (rand() % 2) {
camera->update_func = camera_update_attract_circle;
camera->update_timer = 5;
}
else {
camera->update_func = camera_update_static_follow;
camera->update_timer = 5;
section_t *section = ship->section->next;
for (int i = 0; i < 10; i++) {
section = section->next;
}
camera->section = section;
camera->position = section->center;
camera->position.y -= 500;
}
(camera->update_func)(camera, ship, droid);
}

@ -0,0 +1,32 @@
#ifndef CAMERA_H
#define CAMERA_H
#include "../types.h"
#include "droid.h"
typedef struct camera_t {
vec3_t position;
vec3_t velocity;
vec3_t angle;
vec3_t angular_velocity;
vec3_t last_position;
vec3_t real_velocity;
mat4_t mat;
section_t *section;
bool has_initial_section;
float update_timer;
void (*update_func)(struct camera_t *, ship_t *, droid_t *);
} camera_t;
void camera_init(camera_t *camera, section_t *section);
void camera_update(camera_t *camera, ship_t *ship, droid_t *droid);
void camera_update_race_external(camera_t *, ship_t *camShip, droid_t *);
void camera_update_race_internal(camera_t *, ship_t *camShip, droid_t *);
void camera_update_race_intro(camera_t *, ship_t *camShip, droid_t *);
void camera_update_attract_circle(camera_t *, ship_t *camShip, droid_t *);
void camera_update_attract_internal(camera_t *, ship_t *camShip, droid_t *);
void camera_update_static_follow(camera_t *, ship_t *camShip, droid_t *);
void camera_update_attract_random(camera_t *, ship_t *camShip, droid_t *);
void camera_update_rescue(camera_t *, ship_t *camShip, droid_t *);
#endif

@ -0,0 +1,260 @@
#include "../types.h"
#include "../mem.h"
#include "../system.h"
#include "../utils.h"
#include "object.h"
#include "track.h"
#include "ship.h"
#include "weapon.h"
#include "hud.h"
#include "droid.h"
#include "camera.h"
#include "image.h"
#include "scene.h"
#include "object.h"
#include "game.h"
static Object *droid_model;
void droid_load() {
texture_list_t droid_textures = image_get_compressed_textures("wipeout/common/rescu.cmp");
droid_model = objects_load("wipeout/common/rescu.prm", droid_textures);
}
void droid_init(droid_t *droid, ship_t *ship) {
droid->section = g.track.sections;
while (flags_not(droid->section->flags, SECTION_JUMP)) {
droid->section = droid->section->next;
}
droid->position = vec3_add(ship->position, vec3(0, -200, 0));
droid->velocity = vec3(0, 0, 0);
droid->acceleration = vec3(0, 0, 0);
droid->angle = vec3(0, 0, 0);
droid->angular_velocity = vec3(0, 0, 0);
droid->update_timer = DROID_UPDATE_TIME_INITIAL;
droid->mat = mat4_identity();
droid->cycle_timer = 0;
droid->update_func = droid_update_intro;
droid->sfx_tractor = sfx_reserve_loop(SFX_TRACTOR);
flags_rm(droid->sfx_tractor->flags, SFX_PLAY);
}
void droid_draw(droid_t *droid) {
droid->cycle_timer += system_tick() * M_PI * 2;
Prm prm = {.primitive = droid_model->primitives};
int rf = sin(droid->cycle_timer) * 127 + 128;
int gf = sin(droid->cycle_timer + 0.2) * 127 + 128;
int bf = sin(droid->cycle_timer * 0.5 + 0.1) * 127 + 128;
int r, g, b;
for (int i = 0; i < 11; i++) {
if (i < 2) {
r = 40;
g = gf;
b = 40;
}
else if (i < 6) {
r = bf >> 1;
b = bf;
g = bf >> 1;
}
else {
r = rf;
b = 40;
g = 40;
}
switch (prm.f3->type) {
case PRM_TYPE_GT3:
prm.gt3->colour[0].as_rgba.r = r;
prm.gt3->colour[0].as_rgba.g = g;
prm.gt3->colour[0].as_rgba.b = b;
prm.gt3->colour[1].as_rgba.r = r;
prm.gt3->colour[1].as_rgba.g = g;
prm.gt3->colour[1].as_rgba.b = b;
prm.gt3->colour[2].as_rgba.r = r;
prm.gt3->colour[2].as_rgba.g = g;
prm.gt3->colour[2].as_rgba.b = b;
prm.gt3++;
break;
case PRM_TYPE_GT4:
prm.gt4->colour[0].as_rgba.r = r;
prm.gt4->colour[0].as_rgba.g = g;
prm.gt4->colour[0].as_rgba.b = b;
prm.gt4->colour[1].as_rgba.r = r;
prm.gt4->colour[1].as_rgba.g = g;
prm.gt4->colour[1].as_rgba.b = b;
prm.gt4->colour[2].as_rgba.r = r;
prm.gt4->colour[2].as_rgba.g = g;
prm.gt4->colour[2].as_rgba.b = b;
prm.gt4->colour[3].as_rgba.r = 40;
prm.gt4->colour[3].as_rgba.g = 40;
prm.gt4->colour[3].as_rgba.b = 40;
prm.gt4++;
break;
}
}
mat4_set_translation(&droid->mat, droid->position);
mat4_set_yaw_pitch_roll(&droid->mat, droid->angle);
object_draw(droid_model, &droid->mat);
}
void droid_update(droid_t *droid, ship_t *ship) {
(droid->update_func)(droid, ship);
droid->velocity = vec3_add(droid->velocity, vec3_mulf(droid->acceleration, 30 * system_tick()));
droid->velocity = vec3_sub(droid->velocity, vec3_mulf(droid->velocity, 0.125 * 30 * system_tick()));
droid->position = vec3_add(droid->position, vec3_mulf(droid->velocity, 0.015625 * 30 * system_tick()));
droid->angle = vec3_add(droid->angle, vec3_mulf(droid->angular_velocity, system_tick()));
droid->angle = vec3_wrap_angle(droid->angle);
if (flags_is(droid->sfx_tractor->flags, SFX_PLAY)) {
sfx_set_position(droid->sfx_tractor, droid->position, droid->velocity, 0.5);
}
}
void droid_update_intro(droid_t *droid, ship_t *ship) {
droid->update_timer -= system_tick();
if (droid->update_timer < DROID_UPDATE_TIME_INTRO_3) {
droid->acceleration.x = (-sin(droid->angle.y) * cos(droid->angle.x)) * 0.25 * 4096.0;
droid->acceleration.y = 0;
droid->acceleration.z = (cos(droid->angle.y) * cos(droid->angle.x)) * 0.25 * 4096.0;
droid->angular_velocity.y = 0;
}
else if (droid->update_timer < DROID_UPDATE_TIME_INTRO_2) {
droid->acceleration.x = (-sin(droid->angle.y) * cos(droid->angle.x)) * 0.125 * 4096.0;
droid->acceleration.y = -140;
droid->acceleration.z = (cos(droid->angle.y) * cos(droid->angle.x)) * 0.125 * 4096.0;
droid->angular_velocity.y = (-8.0 / 4096.0) * M_PI * 2 * 30;
}
else if (droid->update_timer < DROID_UPDATE_TIME_INTRO_1) {
droid->acceleration.y -= 90 * system_tick();
droid->angular_velocity.y = (8.0 / 4096.0) * M_PI * 2 * 30;
}
if (droid->update_timer <= 0) {
droid->update_timer = DROID_UPDATE_TIME_INITIAL;
droid->update_func = droid_update_idle;
droid->position.x = droid->section->center.x;
droid->position.y = -3000;
droid->position.z = droid->section->center.z;
}
}
void droid_update_idle(droid_t *droid, ship_t *ship) {
section_t *next = droid->section->next;
vec3_t target = vec3(
(droid->section->center.x + next->center.x) * 0.5,
droid->section->center.y - 3000,
(droid->section->center.z + next->center.z) * 0.5
);
vec3_t target_vector = vec3_sub(target, droid->position);
float target_heading = -atan2(target_vector.x, target_vector.z);
float quickest_turn = target_heading - droid->angle.y;
float turn;
if (droid->angle.y < 0) {
turn = target_heading - (droid->angle.y + M_PI*2);
}
else {
turn = target_heading - (droid->angle.y - M_PI*2);
}
if (fabsf(turn) < fabsf(quickest_turn)) {
droid->angular_velocity.y = turn * 30 / 64.0;
}
else {
droid->angular_velocity.y = quickest_turn * 30.0 / 64.0;
}
droid->acceleration.x = (-sin(droid->angle.y) * cos(droid->angle.x)) * 0.125 * 4096;
droid->acceleration.y = target_vector.y / 64.0;
droid->acceleration.z = (cos(droid->angle.y) * cos(droid->angle.x)) * 0.125 * 4096;
if (flags_is(ship->flags, SHIP_IN_RESCUE)) {
flags_add(droid->sfx_tractor->flags, SFX_PLAY);
droid->update_func = droid_update_rescue;
droid->update_timer = DROID_UPDATE_TIME_INITIAL;
g.camera.update_func = camera_update_rescue;
flags_add(ship->flags, SHIP_VIEW_REMOTE);
if (flags_is(ship->section->flags, SECTION_JUMP)) {
g.camera.section = ship->section->next;
}
else {
g.camera.section = ship->section;
}
// If droid is not nearby the rescue position teleport it in!
if (droid->section != ship->section && droid->section != ship->section->prev) {
droid->section = ship->section;
section_t *next = droid->section->next;
droid->position.x = (droid->section->center.x + next->center.x) * 0.5;
droid->position.y = droid->section->center.y - 3000;
droid->position.z = (droid->section->center.z + next->center.z) * 0.5;
}
flags_rm(ship->flags, SHIP_IN_TOW);
droid->velocity = vec3(0,0,0);
droid->acceleration = vec3(0,0,0);
}
// AdjustDirectionalNote(START_SIREN, 0, 0, (VECTOR){droid->position.x, droid->position.y, droid->position.z});
}
void droid_update_rescue(droid_t *droid, ship_t *ship) {
droid->angular_velocity.y = 0;
droid->angle.y = ship->angle.y;
vec3_t target = vec3(ship->position.x, ship->position.y - 350, ship->position.z);
vec3_t distance = vec3_sub(target, droid->position);
if (flags_is(ship->flags, SHIP_IN_TOW)) {
droid->velocity = vec3(0,0,0);
droid->acceleration = vec3(0,0,0);
droid->position = target;
}
else if (vec3_len(distance) < 8) {
flags_add(ship->flags, SHIP_IN_TOW);
droid->velocity = vec3(0,0,0);
droid->acceleration = vec3(0,0,0);
droid->position = target;
}
else {
droid->velocity = vec3_mulf(distance, 16);
}
// Are we done rescuing?
if (flags_not(ship->flags, SHIP_IN_RESCUE)) {
flags_rm(droid->sfx_tractor->flags, SFX_PLAY);
droid->siren_started = false;
droid->update_func = droid_update_idle;
droid->update_timer = DROID_UPDATE_TIME_INITIAL;
while (flags_not(droid->section->flags, SECTION_JUMP)) {
droid->section = droid->section->prev;
}
}
}

@ -0,0 +1,39 @@
#ifndef DROID_H
#define DROID_H
#include "../types.h"
#include "track.h"
#include "ship.h"
#include "sfx.h"
#define DROID_UPDATE_TIME_INITIAL (800 * (1.0/30.0))
#define DROID_UPDATE_TIME_INTRO_1 (770 * (1.0/30.0))
#define DROID_UPDATE_TIME_INTRO_2 (710 * (1.0/30.0))
#define DROID_UPDATE_TIME_INTRO_3 (400 * (1.0/30.0))
typedef struct droid_t {
section_t *section;
vec3_t position;
vec3_t velocity;
vec3_t acceleration;
vec3_t angle;
vec3_t angular_velocity;
bool siren_started;
float cycle_timer;
float update_timer;
void (*update_func)(struct droid_t *, ship_t *);
mat4_t mat;
Object *model;
sfx_t *sfx_tractor;
} droid_t;
void droid_draw(droid_t *droid);
void droid_load();
void droid_init(droid_t *droid, ship_t *ship);
void droid_update(droid_t *droid, ship_t *ship);
void droid_update_intro(droid_t *droid, ship_t *ship);
void droid_update_idle(droid_t *droid, ship_t *ship);
void droid_update_rescue(droid_t *droid, ship_t *ship);
#endif

@ -0,0 +1,637 @@
#include <string.h>
#include "../mem.h"
#include "../utils.h"
#include "../system.h"
#include "../platform.h"
#include "../input.h"
#include "game.h"
#include "ship.h"
#include "weapon.h"
#include "droid.h"
#include "object.h"
#include "hud.h"
#include "game.h"
#include "sfx.h"
#include "ui.h"
#include "particle.h"
#include "race.h"
#include "main_menu.h"
#include "title.h"
#include "intro.h"
#define TURN_ACCEL(V) NTSC_ACCELERATION(ANGLE_NORM_TO_RADIAN(FIXED_TO_FLOAT(YAW_VELOCITY(V))))
#define TURN_VEL(V) NTSC_VELOCITY(ANGLE_NORM_TO_RADIAN(FIXED_TO_FLOAT(YAW_VELOCITY(V))))
const game_def_t def = {
.race_classes = {
[RACE_CLASS_VENOM] = {.name = "VENOM CLASS"},
[RACE_CLASS_RAPIER] = {.name = "RAPIER CLASS"},
},
.race_types = {
[RACE_TYPE_CHAMPIONSHIP] = {.name = "CHAMPIONSHIP RACE"},
[RACE_TYPE_SINGLE] = {.name = "SINGLE RACE"},
[RACE_TYPE_TIME_TRIAL] = {.name = "TIME TRIAL"},
},
.pilots = {
[PILOT_JOHN_DEKKA] = {.name = "JOHN DEKKA", .portrait = "wipeout/textures/dekka.cmp", .team = 0, .logo_model = 0},
[PILOT_DANIEL_CHANG] = {.name = "DANIEL CHANG", .portrait = "wipeout/textures/chang.cmp", .team = 0, .logo_model = 4},
[PILOT_ARIAL_TETSUO] = {.name = "ARIAL TETSUO", .portrait = "wipeout/textures/arial.cmp", .team = 1, .logo_model = 6},
[PILOT_ANASTASIA_CHEROVOSKI] = {.name = "ANASTASIA CHEROVOSKI", .portrait = "wipeout/textures/anast.cmp", .team = 1, .logo_model = 7},
[PILOT_KEL_SOLAAR] = {.name = "KEL SOLAAR", .portrait = "wipeout/textures/solar.cmp", .team = 2, .logo_model = 2},
[PILOT_ARIAN_TETSUO] = {.name = "ARIAN TETSUO", .portrait = "wipeout/textures/arian.cmp", .team = 2, .logo_model = 5},
[PILOT_SOFIA_DE_LA_RENTE] = {.name = "SOFIA DE LA RENTE", .portrait = "wipeout/textures/sophi.cmp", .team = 3, .logo_model = 1},
[PILOT_PAUL_JACKSON] = {.name = "PAUL JACKSON", .portrait = "wipeout/textures/paul.cmp", .team = 3, .logo_model = 3},
},
.ship_model_to_pilot = {6, 4, 7, 1, 5, 2, 3, 0},
.race_points_for_rank = {9, 7, 5, 3, 2, 1, 0, 0},
// SHIP ATTRIBUTES
// TEAM 1 TEAM 2 TEAM 3 TEAM 4
// Acceleration: *** ***** ** ****
// Top Speed: **** ** **** ***
// Armour: ***** *** **** **
// Turn Rate: ** **** *** *****
.teams = {
[TEAM_AG_SYSTEMS] = {
.name = "AG SYSTEMS",
.logo_model = 2,
.pilots = {0, 1},
.attributes = {
[RACE_CLASS_VENOM] = {.mass = 150, .thrust_max = 790, .resistance = 140, .turn_rate = TURN_ACCEL(160), .turn_rate_max = TURN_VEL(2560), .skid = 12},
[RACE_CLASS_RAPIER] = {.mass = 150, .thrust_max = 1200, .resistance = 140, .turn_rate = TURN_ACCEL(160), .turn_rate_max = TURN_VEL(2560), .skid = 10},
},
},
[TEAM_AURICOM] = {
.name = "AURICOM",
.logo_model = 3,
.pilots = {2, 3},
.attributes = {
[RACE_CLASS_VENOM] = {.mass = 150, .thrust_max = 850, .resistance = 134, .turn_rate = TURN_ACCEL(140), .turn_rate_max = TURN_VEL(1920), .skid = 20},
[RACE_CLASS_RAPIER] = {.mass = 150, .thrust_max = 1400, .resistance = 140, .turn_rate = TURN_ACCEL(120), .turn_rate_max = TURN_VEL(1920), .skid = 14},
},
},
[TEAM_QIREX] = {
.name = "QIREX",
.logo_model = 1,
.pilots = {4, 5},
.attributes = {
[RACE_CLASS_VENOM] = {.mass = 150, .thrust_max = 850, .resistance = 140, .turn_rate = TURN_ACCEL(120), .turn_rate_max = TURN_VEL(1920), .skid = 24},
[RACE_CLASS_RAPIER] = {.mass = 150, .thrust_max = 1400, .resistance = 130, .turn_rate = TURN_ACCEL(140), .turn_rate_max = TURN_VEL(1920), .skid = 16},
},
},
[TEAM_FEISAR] = {
.name = "FEISAR",
.logo_model = 0,
.pilots = {6, 7},
.attributes = {
[RACE_CLASS_VENOM] = {.mass = 150, .thrust_max = 790, .resistance = 134, .turn_rate = TURN_ACCEL(180), .turn_rate_max = TURN_VEL(2560), .skid = 12},
[RACE_CLASS_RAPIER] = {.mass = 150, .thrust_max = 1200, .resistance = 130, .turn_rate = TURN_ACCEL(180), .turn_rate_max = TURN_VEL(2560), .skid = 8},
},
},
},
.ai_settings = {
[RACE_CLASS_VENOM] = {
{.thrust_max = 2550, .thrust_magnitude = 44, .fight_back = 1},
{.thrust_max = 2600, .thrust_magnitude = 45, .fight_back = 1},
{.thrust_max = 2630, .thrust_magnitude = 45, .fight_back = 1},
{.thrust_max = 2660, .thrust_magnitude = 46, .fight_back = 1},
{.thrust_max = 2700, .thrust_magnitude = 47, .fight_back = 1},
{.thrust_max = 2720, .thrust_magnitude = 48, .fight_back = 1},
{.thrust_max = 2750, .thrust_magnitude = 49, .fight_back = 1},
},
[RACE_CLASS_RAPIER] = {
{.thrust_max = 3750, .thrust_magnitude = 50, .fight_back = 1},
{.thrust_max = 3780, .thrust_magnitude = 53, .fight_back = 1},
{.thrust_max = 3800, .thrust_magnitude = 55, .fight_back = 1},
{.thrust_max = 3850, .thrust_magnitude = 57, .fight_back = 1},
{.thrust_max = 3900, .thrust_magnitude = 60, .fight_back = 1},
{.thrust_max = 3950, .thrust_magnitude = 62, .fight_back = 1},
{.thrust_max = 4000, .thrust_magnitude = 65, .fight_back = 1},
},
},
.circuts = {
[CIRCUT_ALTIMA_VII] = {
.name = "ALTIMA VII",
.is_bonus_circut = false,
.settings = {
[RACE_CLASS_VENOM] = {.path = "wipeout/track02/", .start_line_pos = 27, .behind_speed = 300, .spread_base = 80, .spread_factor = 20, .sky_y_offset = -2520},
[RACE_CLASS_RAPIER] = {.path = "wipeout/track03/", .start_line_pos = 27, .behind_speed = 500, .spread_base = 80, .spread_factor = 11, .sky_y_offset = -1930},
}
},
[CIRCUT_KARBONIS_V] = {
.name = "KARBONIS V",
.is_bonus_circut = false,
.settings = {
[RACE_CLASS_VENOM] = {.path = "wipeout/track04/", .start_line_pos = 16, .behind_speed = 200, .spread_base = 10, .spread_factor = 8, .sky_y_offset = -5000},
[RACE_CLASS_RAPIER] = {.path = "wipeout/track05/", .start_line_pos = 16, .behind_speed = 500, .spread_base = 10, .spread_factor = 8, .sky_y_offset = -5000},
}
},
[CIRCUT_TERRAMAX] = {
.name = "TERRAMAX",
.is_bonus_circut = false,
.settings = {
[RACE_CLASS_VENOM] = {.path = "wipeout/track01/", .start_line_pos = 27, .behind_speed = 350, .spread_base = 60, .spread_factor = 11, .sky_y_offset = -820},
[RACE_CLASS_RAPIER] = {.path = "wipeout/track06/", .start_line_pos = 27, .behind_speed = 500, .spread_base = 10, .spread_factor = 8, .sky_y_offset = 0},
}
},
[CIRCUT_KORODERA] = {
.name = "KORODERA",
.is_bonus_circut = false,
.settings = {
[RACE_CLASS_VENOM] = {.path = "wipeout/track12/", .start_line_pos = 16, .behind_speed = 450, .spread_base = 40, .spread_factor = 11, .sky_y_offset = -2120},
[RACE_CLASS_RAPIER] = {.path = "wipeout/track07/", .start_line_pos = 16, .behind_speed = 500, .spread_base = 30, .spread_factor = 11, .sky_y_offset = -2260},
}
},
[CIRCUT_ARRIDOS_IV] = {
.name = "ARRIDOS IV",
.is_bonus_circut = false,
.settings = {
[RACE_CLASS_VENOM] = {.path = "wipeout/track08/", .start_line_pos = 16, .behind_speed = 350, .spread_base = 80, .spread_factor = 15, .sky_y_offset = -40},
[RACE_CLASS_RAPIER] = {.path = "wipeout/track11/", .start_line_pos = 16, .behind_speed = 450, .spread_base = 30, .spread_factor = 11, .sky_y_offset = -240},
}
},
[CIRCUT_SILVERSTREAM] = {
.name = "SILVERSTREAM",
.is_bonus_circut = false,
.settings = {
[RACE_CLASS_VENOM] = {.path = "wipeout/track09/", .start_line_pos = 16, .behind_speed = 150, .spread_base = 10, .spread_factor = 8, .sky_y_offset = -2700},
[RACE_CLASS_RAPIER] = {.path = "wipeout/track13/", .start_line_pos = 16, .behind_speed = 150, .spread_base = 10, .spread_factor = 8, .sky_y_offset = -2700},
}
},
[CIRCUT_FIRESTAR] = {
.name = "FIRESTAR",
.is_bonus_circut = true,
.settings = {
[RACE_CLASS_VENOM] = {.path = "wipeout/track10/", .start_line_pos = 27, .behind_speed = 200, .spread_base = 40, .spread_factor = 11, .sky_y_offset = 0},
[RACE_CLASS_RAPIER] = {.path = "wipeout/track14/", .start_line_pos = 27, .behind_speed = 500, .spread_base = 40, .spread_factor = 11, .sky_y_offset = 0},
}
},
},
.music = {
{.path = "wipeout/music/track01.qoa", .name = "CAIRODROME"},
{.path = "wipeout/music/track02.qoa", .name = "CARDINAL DANCER"},
{.path = "wipeout/music/track03.qoa", .name = "COLD COMFORT"},
{.path = "wipeout/music/track04.qoa", .name = "DOH T"},
{.path = "wipeout/music/track05.qoa", .name = "MESSIJ"},
{.path = "wipeout/music/track06.qoa", .name = "OPERATIQUE"},
{.path = "wipeout/music/track07.qoa", .name = "TENTATIVE"},
{.path = "wipeout/music/track08.qoa", .name = "TRANCEVAAL"},
{.path = "wipeout/music/track09.qoa", .name = "AFRO RIDE"},
{.path = "wipeout/music/track10.qoa", .name = "CHEMICAL BEATS"},
{.path = "wipeout/music/track11.qoa", .name = "WIPEOUT"},
},
.credits = {
"#MANAGING DIRECTORS",
"IAN HETHERINGTON",
"JONATHAN ELLIS",
"#DIRECTOR OF DEVELOPMENT",
"JOHN WHITE",
"#PRODUCERS",
"DOMINIC MALLINSON",
"ANDY YELLAND",
"#PRODUCT MANAGER",
"SUE CAMPBELL",
"#GAME DESIGNER",
"NICK BURCOMBE",
"",
"",
"#PLAYSTATION VERSION",
"#PROGRAMMERS",
"DAVE ROSE",
"ROB SMITH",
"JASON DENTON",
"STEWART SOCKETT",
"#ORIGINAL ARTISTS",
"NICKY CARUS WESTCOTT",
"LAURA GRIEVE",
"LOUISE SMITH",
"DARREN DOUGLAS",
"POL SIGERSON",
"#INTRO SEQUENCE",
"LEE CARUS WESTCOTT",
"#CONCEPTUAL ARTIST",
"JIM BOWERS",
"#ADDITIONAL GRAPHIC DESIGN",
"THE DESIGNERS REPUBLIC",
"#MUSIC",
"ORBITAL",
"CHEMICAL BROTHERS",
"LEFTFIELD",
"COLD STORAGE",
"#SOUND EFFECTS",
"TIM WRIGHT",
"#MANUAL WRITTEN BY",
"DAMON FAIRCLOUGH",
"NICK BURCOMBE",
"#PACKAGING DESIGN",
"THE DESIGNERS REPUBLIC",
"KEITH HOPWOOD",
"",
"",
"#PC VERSION",
"#PROGRAMMERS",
"ANDY YELLAND",
"ANDY SATTERTHWAITE",
"DAVE SMITH",
"MARK KELLY",
"JED ADAMS",
"STEVE WARD",
"CHRIS EDEN",
"SALIM SIWANI",
"#SOUND PROGRAMMING",
"ANDY CROWLEY",
"#MOVIE PROGRAMMING",
"MIKE ANTHONY",
"#CONVERSION ARTISTS",
"JOHN DWYER",
"GARY BURLEY",
"",
"",
"#ATI 3D RAGE VERSION",
"#PRODUCER",
"BILL ALLEN",
"#DEVELOPED BY",
"#BROADSWORD INTERACTIVE LTD",
"STEPHEN ROSE",
"JOHN JONES STEELE",
"",
"",
"#2023 REWRITE",
"PHOBOSLAB",
"DOMINIC SZABLEWSKI",
"",
"",
"#DEVELOPMENT SECRETARY",
"JENNIFER REES",
"",
"",
"#QUALITY ASSURANCE",
"STUART ALLEN",
"CHRIS GRAHAM",
"THOMAS REES",
"BRIAN WALSH",
"CARL BERRY",
"MARK INMAN",
"PAUL TWEEDLE",
"ANTHONY CROSS",
"EDWARD HAY",
"ROB WOLFE",
"",
"",
"#SPECIAL THANKS TO",
"THE HACKERS TEAM MGM",
"SOFTIMAGE",
"SGI",
"GLEN OCONNELL",
"JOANNE GALVIN",
"ALL AT PSYGNOSIS",
},
.congratulations = {
.venom = {
"#WELL DONE",
"",
"VENOM CLASS",
"",
"COMPETENCE ACHIEVED",
"",
"YOU HAVE NOW QUALIFIED",
"",
"FOR THE ULTRA FAST",
"",
"RAPIER CLASS",
"",
"WE RECOMMEND YOU",
"",
"SAVE YOUR CURRENT GAME",
},
.venom_all_circuts = {
"#AMAZING",
"",
"YOU HAVE COMPLETED THE FULL",
"",
"VENOM CLASS CHAMPIONSHIP",
"",
"",
"WELL DONE",
"",
"YOU ARE A GREAT PILOT",
"",
"",
"",
"NOW TAKE ON THE FULL",
"",
"RAPIER CLASS CHAMPIONSHIP",
"",
"",
"#KEEP GOING",
},
.rapier = {
"#CONGRATULATIONS",
"",
"RAPIER CLASS",
"",
"COMPETENCE ACHIEVED",
"",
"YOU NOW HAVE ACCESS TO THE",
"",
"FULL VENOM AND RAPIER",
"",
"CHAMPIONSHIPS WITH THE ",
"",
"NEWLY CONSTRUCTED CIRCUIT",
"",
"FIRESTAR",
"",
"",
"",
"WE RECOMMEND YOU",
"",
"SAVE",
"",
"YOUR CURRENT GAME",
"",
"",
"#GOOD LUCK",
},
.rapier_all_circuts = {
"#AWESOME",
"",
"YOU HAVE BEATEN",
"#WIPEOUT",
"",
"YOU ARE A TRULY",
"",
"AMAZING PILOT",
"",
"",
"",
"#CONGRATULATIONS",
"",
"",
"",
"",
"#A BIG THANKS",
"",
"FROM ALL OF US ON THE TEAM",
"",
"LOOK OUT FOR",
"#WIPEOUT II",
"",
"COMING SOON",
},
}
};
save_t save = {
.magic = SAVE_DATA_MAGIC,
.is_dirty = true,
.sfx_volume = 0.6,
.music_volume = 0.5,
.ui_scale = 0,
.show_fps = false,
.fullscreen = false,
.screen_res = 0,
.post_effect = 0,
.has_rapier_class = true, // for testing; should be false in prod
.has_bonus_circuts = true, // for testing; should be false in prod
.buttons = {
[A_UP] = {INPUT_KEY_UP, INPUT_GAMEPAD_DPAD_UP},
[A_DOWN] = {INPUT_KEY_DOWN, INPUT_GAMEPAD_DPAD_DOWN},
[A_LEFT] = {INPUT_KEY_LEFT, INPUT_GAMEPAD_DPAD_LEFT},
[A_RIGHT] = {INPUT_KEY_RIGHT, INPUT_GAMEPAD_DPAD_RIGHT},
[A_BRAKE_LEFT] = {INPUT_KEY_C, INPUT_GAMEPAD_L_SHOULDER},
[A_BRAKE_RIGHT] = {INPUT_KEY_V, INPUT_GAMEPAD_R_SHOULDER},
[A_THRUST] = {INPUT_KEY_X, INPUT_GAMEPAD_A},
[A_FIRE] = {INPUT_KEY_Z, INPUT_GAMEPAD_X},
[A_CHANGE_VIEW] = {INPUT_KEY_A, INPUT_GAMEPAD_Y},
},
.highscores_name = {0,0,0,0},
.highscores = {
[RACE_CLASS_VENOM] = {
{
[HIGHSCORE_TAB_RACE] = {.lap_record = 85.83, .entries = {{"WIP", 254.50},{"EOU", 271.17},{"TPC", 289.50},{"NOT", 294.50},{"PSX", 314.50}}},
[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 85.83, .entries = {{"MVE", 254.50},{"ALM", 271.17},{"POL", 289.50},{"NIK", 294.50},{"DAR", 314.50}}},
},
{
[HIGHSCORE_TAB_RACE] = {.lap_record = 55.33, .entries = {{"AJY", 159.33},{"AJS", 172.67},{"DLS", 191.00},{"MAK", 207.67},{"JED", 219.33}}},
[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 55.33, .entries = {{"DAR", 159.33},{"STU", 172.67},{"MOC", 191.00},{"DOM", 207.67},{"NIK", 219.33}}},
},
{
[HIGHSCORE_TAB_RACE] = {.lap_record = 57.5, .entries = {{ "JD", 171.00},{"AJC", 189.33},{"MSA", 202.67},{ "SD", 219.33},{"TIM", 232.67}}},
[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 57.5, .entries = {{"PHO", 171.00},{"ENI", 189.33},{ "XR", 202.67},{"ISI", 219.33},{ "NG", 232.67}}},
},
{
[HIGHSCORE_TAB_RACE] = {.lap_record = 85.17, .entries = {{"POL", 251.33},{"DAR", 263.00},{"JAS", 283.00},{"ROB", 294.67},{"DJR", 314.82}}},
[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 85.17, .entries = {{"DOM", 251.33},{"DJR", 263.00},{"MPI", 283.00},{"GOC", 294.67},{"SUE", 314.82}}},
},
{
[HIGHSCORE_TAB_RACE] = {.lap_record = 80.17, .entries = {{"NIK", 236.17},{"SAL", 253.17},{"DOM", 262.33},{ "LG", 282.67},{"LNK", 298.17}}},
[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 80.17, .entries = {{"NIK", 236.17},{"ROB", 253.17},{ "AM", 262.33},{"JAS", 282.67},{"DAR", 298.17}}},
},
{
[HIGHSCORE_TAB_RACE] = {.lap_record = 61.67, .entries = {{"HAN", 182.33},{"PER", 196.33},{"FEC", 214.83},{"TPI", 228.83},{"ZZA", 244.33}}},
[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 61.67, .entries = {{ "FC", 182.33},{"SUE", 196.33},{"ROB", 214.83},{"JEN", 228.83},{ "NT", 244.33}}},
},
{
[HIGHSCORE_TAB_RACE] = {.lap_record = 63.83, .entries = {{"CAN", 195.40},{"WEH", 209.23},{"AVE", 227.90},{"ABO", 239.90},{"NUS", 240.73}}},
[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 63.83, .entries = {{"DJR", 195.40},{"NIK", 209.23},{"JAS", 227.90},{"NCW", 239.90},{"LOU", 240.73}}},
},
},
[RACE_CLASS_RAPIER] = {
{
[HIGHSCORE_TAB_RACE] = {.lap_record = 69.50, .entries = {{"AJY", 200.67},{"DLS", 213.50},{"AJS", 228.67},{"MAK", 247.67},{"JED", 263.00}}},
[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 69.50, .entries = {{"NCW", 200.67},{"LEE", 213.50},{"STU", 228.67},{"JAS", 247.67},{"ROB", 263.00}}},
},
{
[HIGHSCORE_TAB_RACE] = {.lap_record = 47.33, .entries = {{"BOR", 134.58},{"ING", 147.00},{"HIS", 162.25},{"COR", 183.08},{ "ES", 198.25}}},
[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 47.33, .entries = {{"NIK", 134.58},{"POL", 147.00},{"DAR", 162.25},{"STU", 183.08},{"ROB", 198.25}}},
},
{
[HIGHSCORE_TAB_RACE] = {.lap_record = 47.83, .entries = {{"AJS", 142.08},{"DLS", 159.42},{"MAK", 178.08},{"JED", 190.25},{"AJY", 206.58}}},
[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 47.83, .entries = {{"POL", 142.08},{"JIM", 159.42},{"TIM", 178.08},{"MOC", 190.25},{ "PC", 206.58}}},
},
{
[HIGHSCORE_TAB_RACE] = {.lap_record = 76.75, .entries = {{"DLS", 224.17},{"DJR", 237.00},{"LEE", 257.50},{"MOC", 272.83},{"MPI", 285.17}}},
[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 76.75, .entries = {{"TIM", 224.17},{"JIM", 237.00},{"NIK", 257.50},{"JAS", 272.83},{ "LG", 285.17}}},
},
{
[HIGHSCORE_TAB_RACE] = {.lap_record = 65.75, .entries = {{"MAK", 191.00},{"STU", 203.67},{"JAS", 221.83},{"ROB", 239.00},{"DOM", 254.50}}},
[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 65.75, .entries = {{ "LG", 191.00},{"LOU", 203.67},{"JIM", 221.83},{"HAN", 239.00},{ "NT", 254.50}}},
},
{
[HIGHSCORE_TAB_RACE] = {.lap_record = 59.23, .entries = {{"JED", 156.67},{"NCW", 170.33},{"LOU", 188.83},{"DAR", 201.00},{"POL", 221.50}}},
[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 59.23, .entries = {{"STU", 156.67},{"DAV", 170.33},{"DOM", 188.83},{"MOR", 201.00},{"GAN", 221.50}}},
},
{
[HIGHSCORE_TAB_RACE] = {.lap_record = 55.00, .entries = {{ "PC", 162.42},{"POL", 179.58},{"DAR", 194.75},{"DAR", 208.92},{"MSC", 224.58}}},
[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 55.00, .entries = {{"THA", 162.42},{"NKS", 179.58},{"FOR", 194.75},{"PLA", 208.92},{"YIN", 224.58}}},
}
}
}
};
game_t g = {0};
struct {
void (*init)();
void (*update)();
} game_scenes[] = {
[GAME_SCENE_INTRO] = {intro_init, intro_update},
[GAME_SCENE_TITLE] = {title_init, title_update},
[GAME_SCENE_MAIN_MENU] = {main_menu_init, main_menu_update},
[GAME_SCENE_RACE] = {race_init, race_update},
};
static game_scene_t scene_current = GAME_SCENE_NONE;
static game_scene_t scene_next = GAME_SCENE_NONE;
static int global_textures_len = 0;
static void *global_mem_mark = 0;
void game_init() {
if (file_exists("save.dat")) {
uint32_t size;
save_t *save_file = (save_t *)file_load("save.dat", &size);
if (size == sizeof(save_t) && save_file->magic == SAVE_DATA_MAGIC) {
printf("load save data success\n");
memcpy(&save, save_file, sizeof(save_t));
}
mem_temp_free(save_file);
}
platform_set_fullscreen(save.fullscreen);
render_set_resolution(save.screen_res);
render_set_post_effect(save.post_effect);
srand((int)(platform_now() * 100));
ui_load();
sfx_load();
hud_load();
ships_load();
droid_load();
particles_load();
weapons_load();
global_textures_len = render_textures_len();
global_mem_mark = mem_mark();
sfx_music_mode(SFX_MUSIC_PAUSED);
sfx_music_play(rand_int(0, len(def.music)));
// System binds; always fixed
// Keyboard
input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_UP, A_MENU_UP);
input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_DOWN, A_MENU_DOWN);
input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_LEFT, A_MENU_LEFT);
input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_RIGHT, A_MENU_RIGHT);
input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_BACKSPACE, A_MENU_BACK);
input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_C, A_MENU_BACK);
input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_V, A_MENU_BACK);
input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_X, A_MENU_SELECT);
input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_RETURN, A_MENU_START);
input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_ESCAPE, A_MENU_QUIT);
// Gamepad
input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_DPAD_UP, A_MENU_UP);
input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_DPAD_DOWN, A_MENU_DOWN);
input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_DPAD_LEFT, A_MENU_LEFT);
input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_DPAD_RIGHT, A_MENU_RIGHT);
input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_L_STICK_UP, A_MENU_UP);
input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_L_STICK_DOWN, A_MENU_DOWN);
input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_L_STICK_LEFT, A_MENU_LEFT);
input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_L_STICK_RIGHT, A_MENU_RIGHT);
input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_X, A_MENU_BACK);
input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_B, A_MENU_BACK);
input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_A, A_MENU_SELECT);
input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_START, A_MENU_START);
// User defined, loaded from the save struct
for (int action = 0; action < len(save.buttons); action++) {
if (save.buttons[action][0] != INPUT_INVALID) {
input_bind(INPUT_LAYER_USER, save.buttons[action][0], action);
}
if (save.buttons[action][1] != INPUT_INVALID) {
input_bind(INPUT_LAYER_USER, save.buttons[action][1], action);
}
}
game_set_scene(GAME_SCENE_INTRO);
}
void game_set_scene(game_scene_t scene) {
sfx_reset();
scene_next = scene;
}
void game_reset_championship() {
for (int i = 0; i < len(g.championship_ranks); i++) {
g.championship_ranks[i].points = 0;
g.championship_ranks[i].pilot = i;
}
g.lives = NUM_LIVES;
}
void game_update() {
double frame_start_time = platform_now();
int sh = render_size().y;
int scale = max(1, sh >= 720 ? sh / 360 : sh / 240);
if (save.ui_scale && save.ui_scale < scale) {
scale = save.ui_scale;
}
ui_set_scale(scale);
if (scene_next != GAME_SCENE_NONE) {
scene_current = scene_next;
scene_next = GAME_SCENE_NONE;
render_textures_reset(global_textures_len);
mem_reset(global_mem_mark);
system_reset_cycle_time();
if (scene_current != GAME_SCENE_NONE) {
game_scenes[scene_current].init();
}
}
if (scene_current != GAME_SCENE_NONE) {
game_scenes[scene_current].update();
}
if (save.is_dirty) {
// FIXME: use a text based format?
// FIXME: this should probably run async somewhere
save.is_dirty = false;
file_store("save.dat", &save, sizeof(save_t));
printf("wrote save.dat\n");
}
double now = platform_now();
g.frame_time = now - frame_start_time;
if (g.frame_time > 0) {
g.frame_rate = ((double)g.frame_rate * 0.95) + (1.0/g.frame_time) * 0.05;
}
}

@ -0,0 +1,270 @@
#ifndef GAME_H
#define GAME_H
#include "../types.h"
#include "droid.h"
#include "ship.h"
#include "camera.h"
#include "track.h"
#define NUM_AI_OPPONENTS 7
#define NUM_PILOTS_PER_TEAM 2
#define NUM_NON_BONUS_CIRCUTS 6
#define NUM_MUSIC_TRACKS 11
#define NUM_HIGHSCORES 5
#define NUM_LAPS 3
#define NUM_LIVES 3
#define QUALIFYING_RANK 3
#define SAVE_DATA_MAGIC 0x64736f77
typedef enum {
A_UP,
A_DOWN,
A_LEFT,
A_RIGHT,
A_BRAKE_LEFT,
A_BRAKE_RIGHT,
A_THRUST,
A_FIRE,
A_CHANGE_VIEW,
NUM_GAME_ACTIONS,
A_MENU_UP,
A_MENU_DOWN,
A_MENU_LEFT,
A_MENU_RIGHT,
A_MENU_BACK,
A_MENU_SELECT,
A_MENU_START,
A_MENU_QUIT,
} action_t;
typedef enum {
GAME_SCENE_INTRO,
GAME_SCENE_TITLE,
GAME_SCENE_MAIN_MENU,
GAME_SCENE_HIGHSCORES,
GAME_SCENE_RACE,
GAME_SCENE_NONE,
NUM_GAME_SCENES
} game_scene_t;
enum race_class {
RACE_CLASS_VENOM,
RACE_CLASS_RAPIER,
NUM_RACE_CLASSES
};
enum race_type {
RACE_TYPE_CHAMPIONSHIP,
RACE_TYPE_SINGLE,
RACE_TYPE_TIME_TRIAL,
NUM_RACE_TYPES,
};
enum highscore_tab {
HIGHSCORE_TAB_TIME_TRIAL,
HIGHSCORE_TAB_RACE,
NUM_HIGHSCORE_TABS
};
enum pilot {
PILOT_JOHN_DEKKA,
PILOT_DANIEL_CHANG,
PILOT_ARIAL_TETSUO,
PILOT_ANASTASIA_CHEROVOSKI,
PILOT_KEL_SOLAAR,
PILOT_ARIAN_TETSUO,
PILOT_SOFIA_DE_LA_RENTE,
PILOT_PAUL_JACKSON,
NUM_PILOTS
};
enum team {
TEAM_AG_SYSTEMS,
TEAM_AURICOM,
TEAM_QIREX,
TEAM_FEISAR,
NUM_TEAMS
};
enum circut {
CIRCUT_ALTIMA_VII,
CIRCUT_KARBONIS_V,
CIRCUT_TERRAMAX,
CIRCUT_KORODERA,
CIRCUT_ARRIDOS_IV,
CIRCUT_SILVERSTREAM,
CIRCUT_FIRESTAR,
NUM_CIRCUTS
};
// Game definitions
typedef struct {
char *name;
} race_class_t;
typedef struct {
char *name;
} race_type_t;
typedef struct {
char *name;
char *portrait;
int logo_model;
int team;
} pilot_t;
typedef struct {
float thrust_max;
float thrust_magnitude;
bool fight_back;
} ai_setting_t;
typedef struct {
float mass;
float thrust_max;
float resistance;
float turn_rate;
float turn_rate_max;
float skid;
} team_attributes_t;
typedef struct {
char *name;
int logo_model;
int pilots[NUM_PILOTS_PER_TEAM];
team_attributes_t attributes[NUM_RACE_CLASSES];
} team_t;
typedef struct {
char *path;
float start_line_pos;
float behind_speed;
float spread_base;
float spread_factor;
float sky_y_offset;
} circut_settings_t;
typedef struct {
char *name;
bool is_bonus_circut;
circut_settings_t settings[NUM_RACE_CLASSES];
} circut_t;
typedef struct {
char *path;
char *name;
} music_track_t;
typedef struct {
race_class_t race_classes[NUM_RACE_CLASSES];
race_type_t race_types[NUM_RACE_TYPES];
pilot_t pilots[NUM_PILOTS];
team_t teams[NUM_TEAMS];
ai_setting_t ai_settings[NUM_RACE_CLASSES][NUM_AI_OPPONENTS];
circut_t circuts[NUM_CIRCUTS];
int ship_model_to_pilot[NUM_PILOTS];
int race_points_for_rank[NUM_PILOTS];
music_track_t music[NUM_MUSIC_TRACKS];
char *credits[104];
struct {
char *venom[15];
char *venom_all_circuts[19];
char *rapier[26];
char *rapier_all_circuts[24];
} congratulations;
} game_def_t;
// Running game data
typedef struct {
uint16_t pilot;
uint16_t points;
} pilot_points_t;
typedef struct {
float frame_time;
float frame_rate;
int race_class;
int race_type;
int highscore_tab;
int team;
int pilot;
int circut;
bool is_attract_mode;
bool show_credits;
bool is_new_lap_record;
bool is_new_race_record;
float best_lap;
float race_time;
int lives;
int race_position;
float lap_times[NUM_PILOTS][NUM_LAPS];
pilot_points_t race_ranks[NUM_PILOTS];
pilot_points_t championship_ranks[NUM_PILOTS];
camera_t camera;
droid_t droid;
ship_t ships[NUM_PILOTS];
track_t track;
} game_t;
// Save Data
typedef struct {
char name[4];
float time;
} highscores_entry_t;
typedef struct {
highscores_entry_t entries[NUM_HIGHSCORES];
float lap_record;
} highscores_t;
typedef struct {
uint32_t magic;
bool is_dirty;
float sfx_volume;
float music_volume;
uint8_t ui_scale;
bool show_fps;
bool fullscreen;
int screen_res;
int post_effect;
uint32_t has_rapier_class;
uint32_t has_bonus_circuts;
uint8_t buttons[NUM_GAME_ACTIONS][2];
char highscores_name[4];
highscores_t highscores[NUM_RACE_CLASSES][NUM_CIRCUTS][NUM_HIGHSCORE_TABS];
} save_t;
extern const game_def_t def;
extern game_t g;
extern save_t save;
void game_init();
void game_set_scene(game_scene_t scene);
void game_reset_championship();
void game_update();
#endif

@ -0,0 +1,244 @@
#include "../types.h"
#include "../mem.h"
#include "../utils.h"
#include "../system.h"
#include "object.h"
#include "track.h"
#include "ship.h"
#include "weapon.h"
#include "hud.h"
#include "droid.h"
#include "camera.h"
#include "image.h"
#include "ship_ai.h"
#include "game.h"
#include "ui.h"
static texture_list_t weapon_icon_textures;
static uint16_t target_reticle;
typedef struct {
vec2i_t offset;
uint16_t height;
rgba_t color;
} speedo_bar_t;
const struct {
uint16_t width;
uint16_t skew;
speedo_bar_t bars[13];
} speedo = {
.width = 121,
.skew = 2,
.bars = {
{{.x = 6, .y = 12}, .height = 10, .color = rgba( 66, 16, 49, 255)},
{{.x = 13, .y = 12}, .height = 10, .color = rgba(115, 33, 90, 255)},
{{.x = 20, .y = 12}, .height = 10, .color = rgba(132, 58, 164, 255)},
{{.x = 27, .y = 12}, .height = 10, .color = rgba( 99, 90, 197, 255)},
{{.x = 34, .y = 12}, .height = 10, .color = rgba( 74, 148, 181, 255)},
{{.x = 41, .y = 12}, .height = 10, .color = rgba( 66, 173, 115, 255)},
{{.x = 50, .y = 10}, .height = 12, .color = rgba( 99, 206, 58, 255)},
{{.x = 59, .y = 8}, .height = 12, .color = rgba(189, 206, 41, 255)},
{{.x = 69, .y = 5}, .height = 13, .color = rgba(247, 140, 33, 255)},
{{.x = 81, .y = 2}, .height = 15, .color = rgba(255, 197, 49, 255)},
{{.x = 95, .y = 1}, .height = 16, .color = rgba(255, 222, 115, 255)},
{{.x = 110, .y = 1}, .height = 16, .color = rgba(255, 239, 181, 255)},
{{.x = 126, .y = 1}, .height = 16, .color = rgba(255, 255, 255, 255)}
}
};
static uint16_t speedo_facia_texture;
void hud_load() {
speedo_facia_texture = image_get_texture("wipeout/textures/speedo.tim");
target_reticle = image_get_texture_semi_trans("wipeout/textures/target2.tim");
weapon_icon_textures = image_get_compressed_textures("wipeout/common/wicons.cmp");
}
static void hud_draw_speedo_bar(vec2i_t *pos, const speedo_bar_t *a, const speedo_bar_t *b, float f, rgba_t color_override) {
rgba_t left_color, right_color;
if (color_override.as_uint32 > 0) {
left_color = color_override;
right_color = color_override;
}
else {
left_color = a->color;
right_color = rgba(
lerp(a->color.as_rgba.r, b->color.as_rgba.r, f),
lerp(a->color.as_rgba.g, b->color.as_rgba.g, f),
lerp(a->color.as_rgba.b, b->color.as_rgba.b, f),
lerp(a->color.as_rgba.a, b->color.as_rgba.a, f)
);
}
float right_h = lerp(a->height, b->height, f);
vec2i_t top_left = vec2i(a->offset.x + 1, a->offset.y);
vec2i_t bottom_left = vec2i(a->offset.x + 1 - a->height / speedo.skew, a->offset.y + a->height);
vec2i_t top_right = vec2i(lerp(a->offset.x + 1, b->offset.x, f), lerp(a->offset.y, b->offset.y, f));
vec2i_t bottom_right = vec2i(top_right.x - right_h / speedo.skew, top_right.y + right_h);
top_left = ui_scaled(top_left);
bottom_left = ui_scaled(bottom_left);
top_right = ui_scaled(top_right);
bottom_right = ui_scaled(bottom_right);
render_push_tris((tris_t) {
.vertices = {
{
.pos = {pos->x + bottom_left.x, pos->y + bottom_left.y, 0},
.uv = {0, 0},
.color = left_color
},
{
.pos = {pos->x + top_right.x, pos->y + top_right.y, 0},
.uv = {0, 0},
.color = right_color
},
{
.pos = {pos->x + top_left.x, pos->y + top_left.y, 0},
.uv = {0, 0},
.color = left_color
},
}
}, RENDER_NO_TEXTURE);
render_push_tris((tris_t) {
.vertices = {
{
.pos = {pos->x + bottom_right.x, pos->y + bottom_right.y, 0},
.uv = {0, 0},
.color = right_color
},
{
.pos = {pos->x + top_right.x, pos->y + top_right.y, 0},
.uv = {0, 0},
.color = right_color
},
{
.pos = {pos->x + bottom_left.x, pos->y + bottom_left.y, 0},
.uv = {0, 0},
.color = left_color
},
}
}, RENDER_NO_TEXTURE);
}
static void hud_draw_speedo_bars(vec2i_t *pos, float f, rgba_t color_override) {
if (f <= 0) {
return;
}
if (f - floor(f) > 0.9) {
f = ceil(f);
}
if (f > 13) {
f = 13;
}
int bars = f;
for (int i = 1; i < bars; i++) {
hud_draw_speedo_bar(pos, &speedo.bars[i - 1], &speedo.bars[i], 1, color_override);
}
if (bars > 12) {
return;
}
float last_bar_fraction = f - bars + 0.1;
if (last_bar_fraction <= 0) {
return;
}
if (last_bar_fraction > 1) {
last_bar_fraction = 1;
}
int last_bar = bars == 0 ? 1 : bars;
hud_draw_speedo_bar(pos, &speedo.bars[last_bar - 1], &speedo.bars[last_bar], last_bar_fraction, color_override);
}
static void hud_draw_speedo(int speed, int thrust) {
vec2i_t facia_pos = ui_scaled_pos(UI_POS_BOTTOM | UI_POS_RIGHT, vec2i(-141, -45));
vec2i_t bar_pos = ui_scaled_pos(UI_POS_BOTTOM | UI_POS_RIGHT, vec2i(-141, -40));
hud_draw_speedo_bars(&bar_pos, thrust / 65.0, rgba(255, 0, 0, 128));
hud_draw_speedo_bars(&bar_pos, speed / 2166.0, rgba(0, 0, 0, 0));
render_push_2d(facia_pos, ui_scaled(render_texture_size(speedo_facia_texture)), rgba(128, 128, 128, 255), speedo_facia_texture);
}
static void hud_draw_target_icon(vec3_t position) {
vec2i_t screen_size = render_size();
vec2i_t size = ui_scaled(render_texture_size(target_reticle));
vec3_t projected = render_transform(position);
vec2i_t pos = vec2i(
(( projected.x + 1.0) / 2.0) * screen_size.x - size.x / 2,
((-projected.y + 1.0) / 2.0) * screen_size.y - size.y / 2
);
render_push_2d(pos, size, rgba(128, 128, 128, 128), target_reticle);
}
void hud_draw(ship_t *ship) {
// Current lap time
if (ship->lap >= 0) {
ui_draw_time(ship->lap_time, ui_scaled_pos(UI_POS_BOTTOM | UI_POS_LEFT, vec2i(16, -30)), UI_SIZE_16, UI_COLOR_DEFAULT);
for (int i = 0; i < ship->lap && i < NUM_LAPS-1; i++) {
ui_draw_time(g.lap_times[ship->pilot][i], ui_scaled_pos(UI_POS_BOTTOM | UI_POS_LEFT, vec2i(16, -45 - (10 * i))), UI_SIZE_8, UI_COLOR_ACCENT);
}
}
// Current Lap
int display_lap = max(0, ship->lap + 1);
ui_draw_text("LAP", ui_scaled(vec2i(15, 8)), UI_SIZE_8, UI_COLOR_ACCENT);
ui_draw_number(display_lap, ui_scaled(vec2i(10, 19)), UI_SIZE_16, UI_COLOR_DEFAULT);
int width = ui_char_width('0' + display_lap, UI_SIZE_16);
ui_draw_text("OF", ui_scaled(vec2i((10 + width), 27)), UI_SIZE_8, UI_COLOR_ACCENT);
ui_draw_number(NUM_LAPS, ui_scaled(vec2i((32 + width), 19)), UI_SIZE_16, UI_COLOR_DEFAULT);
// Race Position
if (g.race_type != RACE_TYPE_TIME_TRIAL) {
ui_draw_text("POSITION", ui_scaled_pos(UI_POS_TOP | UI_POS_RIGHT, vec2i(-90, 8)), UI_SIZE_8, UI_COLOR_ACCENT);
ui_draw_number(ship->position_rank, ui_scaled_pos(UI_POS_TOP | UI_POS_RIGHT, vec2i(-60, 19)), UI_SIZE_16, UI_COLOR_DEFAULT);
}
// Framerate
if (save.show_fps) {
ui_draw_text("FPS", ui_scaled(vec2i(16, 78)), UI_SIZE_8, UI_COLOR_ACCENT);
ui_draw_number((int)(g.frame_rate), ui_scaled(vec2i(16, 90)), UI_SIZE_8, UI_COLOR_DEFAULT);
}
// Lap Record
ui_draw_text("LAP RECORD", ui_scaled(vec2i(15, 43)), UI_SIZE_8, UI_COLOR_ACCENT);
ui_draw_time(save.highscores[g.race_class][g.circut][g.highscore_tab].lap_record, ui_scaled(vec2i(15, 55)), UI_SIZE_8, UI_COLOR_DEFAULT);
// Wrong way
if (flags_not(ship->flags, SHIP_DIRECTION_FORWARD)) {
ui_draw_text_centered("WRONG WAY", ui_scaled_pos(UI_POS_MIDDLE | UI_POS_CENTER, vec2i(-20, 0)), UI_SIZE_16, UI_COLOR_ACCENT);
}
// Speedo
int speedo_speed = (g.camera.update_func == camera_update_attract_internal)
? ship->speed * 7
: ship->speed;
hud_draw_speedo(speedo_speed, ship->thrust_mag);
// Weapon icon
if (ship->weapon_type != WEAPON_TYPE_NONE) {
vec2i_t pos = ui_scaled_pos(UI_POS_TOP | UI_POS_CENTER, vec2i(-16, 20));
vec2i_t size = ui_scaled(vec2i(32, 32));
uint16_t icon = texture_from_list(weapon_icon_textures, ship->weapon_type-1);
render_push_2d(pos, size, rgba(128,128,128,255), icon);
}
// Lives
if (g.race_type == RACE_TYPE_CHAMPIONSHIP) {
for (int i = 0; i < g.lives; i++) {
ui_draw_icon(UI_ICON_STAR, ui_scaled_pos(UI_POS_BOTTOM | UI_POS_RIGHT, vec2i(-26 - 13 * i, -50)), UI_COLOR_DEFAULT);
}
}
// Weapon target reticle
if (ship->weapon_target) {
hud_draw_target_icon(ship->weapon_target->position);
}
}

@ -0,0 +1,9 @@
#ifndef HUD_H
#define HUD_H
#include "ship.h"
void hud_load();
void hud_draw(ship_t *ship);
#endif

@ -0,0 +1,320 @@
#include "../types.h"
#include "../mem.h"
#include "../utils.h"
#include "object.h"
#include "track.h"
#include "ship.h"
#include "weapon.h"
#include "droid.h"
#include "camera.h"
#include "object.h"
#include "scene.h"
#include "game.h"
#include "hud.h"
#include "image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "../libs/stb_image_write.h"
#define TIM_TYPE_PALETTED_4_BPP 0x08
#define TIM_TYPE_PALETTED_8_BPP 0x09
#define TIM_TYPE_TRUE_COLOR_16_BPP 0x02
static inline rgba_t tim_16bit_to_rgba(uint16_t c, bool transparent_bit) {
return rgba(
((c >> 0) & 0x1f) << 3,
((c >> 5) & 0x1f) << 3,
((c >> 10) & 0x1f) << 3,
(c == 0
? 0x00
: transparent_bit && (c & 0x7fff) == 0 ? 0x00 : 0xff
)
);
}
image_t *image_alloc(uint32_t width, uint32_t height) {
image_t *image = mem_temp_alloc(sizeof(image_t) + width * height * sizeof(rgba_t));
image->width = width;
image->height = height;
image->pixels = (rgba_t *)(((uint8_t *)image) + sizeof(image_t));
return image;
}
image_t *image_load_from_bytes(uint8_t *bytes, bool transparent) {
uint32_t p = 0;
uint32_t magic = get_i32_le(bytes, &p);
uint32_t type = get_i32_le(bytes, &p);
uint16_t *palette = NULL;
if (
type == TIM_TYPE_PALETTED_4_BPP ||
type == TIM_TYPE_PALETTED_8_BPP
) {
uint32_t header_length = get_i32_le(bytes, &p);
uint16_t palette_x = get_i16_le(bytes, &p);
uint16_t palette_y = get_i16_le(bytes, &p);
uint16_t palette_colors = get_i16_le(bytes, &p);
uint16_t palettes = get_i16_le(bytes, &p);
palette = (uint16_t *)(bytes + p);
p += palette_colors * 2;
}
uint32_t data_size = get_i32_le(bytes, &p);
int32_t pixels_per_16bit = 1;
if (type == TIM_TYPE_PALETTED_8_BPP) {
pixels_per_16bit = 2;
}
else if (type == TIM_TYPE_PALETTED_4_BPP) {
pixels_per_16bit = 4;
}
uint16_t skip_x = get_i16_le(bytes, &p);
uint16_t skip_y = get_i16_le(bytes, &p);
uint16_t entries_per_row = get_i16_le(bytes, &p);
uint16_t rows = get_i16_le(bytes, &p);
int32_t width = entries_per_row * pixels_per_16bit;
int32_t height = rows;
int32_t entries = entries_per_row * rows;
image_t *image = image_alloc(width, height);
int32_t pixel_pos = 0;
if (type == TIM_TYPE_TRUE_COLOR_16_BPP) {
for (int i = 0; i < entries; i++) {
image->pixels[pixel_pos++] = tim_16bit_to_rgba(get_i16_le(bytes, &p), transparent);
}
}
else if (type == TIM_TYPE_PALETTED_8_BPP) {
for (int i = 0; i < entries; i++) {
int32_t palette_pos = get_i16_le(bytes, &p);
image->pixels[pixel_pos++] = tim_16bit_to_rgba(palette[(palette_pos >> 0) & 0xff], transparent);
image->pixels[pixel_pos++] = tim_16bit_to_rgba(palette[(palette_pos >> 8) & 0xff], transparent);
}
}
else if (type == TIM_TYPE_PALETTED_4_BPP) {
for (int i = 0; i < entries; i++) {
int32_t palette_pos = get_i16_le(bytes, &p);
image->pixels[pixel_pos++] = tim_16bit_to_rgba(palette[(palette_pos >> 0) & 0xf], transparent);
image->pixels[pixel_pos++] = tim_16bit_to_rgba(palette[(palette_pos >> 4) & 0xf], transparent);
image->pixels[pixel_pos++] = tim_16bit_to_rgba(palette[(palette_pos >> 8) & 0xf], transparent);
image->pixels[pixel_pos++] = tim_16bit_to_rgba(palette[(palette_pos >> 12) & 0xf], transparent);
}
}
return image;
}
#define LZSS_INDEX_BIT_COUNT 13
#define LZSS_LENGTH_BIT_COUNT 4
#define LZSS_WINDOW_SIZE (1 << LZSS_INDEX_BIT_COUNT)
#define LZSS_BREAK_EVEN ((1 + LZSS_INDEX_BIT_COUNT + LZSS_LENGTH_BIT_COUNT) / 9)
#define LZSS_END_OF_STREAM 0
#define LZSS_MOD_WINDOW(a) ((a) & (LZSS_WINDOW_SIZE - 1))
void lzss_decompress(uint8_t *in_data, uint8_t *out_data) {
int16_t i;
int16_t current_position;
uint8_t cc;
int16_t match_length;
int16_t match_position;
uint32_t mask;
uint32_t return_value;
uint8_t in_bfile_mask;
int16_t in_bfile_rack;
int16_t value;
uint8_t window[LZSS_WINDOW_SIZE];
in_bfile_rack = 0;
in_bfile_mask = 0x80;
current_position = 1;
while (true) {
if (in_bfile_mask == 0x80) {
in_bfile_rack = (int16_t) * in_data++;
}
value = in_bfile_rack & in_bfile_mask;
in_bfile_mask >>= 1;
if (in_bfile_mask == 0) {
in_bfile_mask = 0x80;
}
if (value) {
mask = 1L << (8 - 1);
return_value = 0;
while (mask != 0) {
if (in_bfile_mask == 0x80) {
in_bfile_rack = (int16_t) * in_data++;
}
if (in_bfile_rack & in_bfile_mask) {
return_value |= mask;
}
mask >>= 1;
in_bfile_mask >>= 1;
if (in_bfile_mask == 0) {
in_bfile_mask = 0x80;
}
}
cc = (uint8_t) return_value;
*out_data++ = cc;
window[ current_position ] = cc;
current_position = LZSS_MOD_WINDOW(current_position + 1);
}
else {
mask = 1L << (LZSS_INDEX_BIT_COUNT - 1);
return_value = 0;
while (mask != 0) {
if (in_bfile_mask == 0x80) {
in_bfile_rack = (int16_t) * in_data++;
}
if (in_bfile_rack & in_bfile_mask) {
return_value |= mask;
}
mask >>= 1;
in_bfile_mask >>= 1;
if (in_bfile_mask == 0) {
in_bfile_mask = 0x80;
}
}
match_position = (int16_t) return_value;
if (match_position == LZSS_END_OF_STREAM) {
break;
}
mask = 1L << (LZSS_LENGTH_BIT_COUNT - 1);
return_value = 0;
while (mask != 0) {
if (in_bfile_mask == 0x80) {
in_bfile_rack = (int16_t) * in_data++;
}
if (in_bfile_rack & in_bfile_mask) {
return_value |= mask;
}
mask >>= 1;
in_bfile_mask >>= 1;
if (in_bfile_mask == 0) {
in_bfile_mask = 0x80;
}
}
match_length = (int16_t) return_value;
match_length += LZSS_BREAK_EVEN;
for (i = 0 ; i <= match_length ; i++) {
cc = window[LZSS_MOD_WINDOW(match_position + i)];
*out_data++ = cc;
window[current_position] = cc;
current_position = LZSS_MOD_WINDOW(current_position + 1);
}
}
}
}
cmp_t *image_load_compressed(char *name) {
printf("load cmp %s\n", name);
uint32_t compressed_size;
uint8_t *compressed_bytes = file_load(name, &compressed_size);
uint32_t p = 0;
int32_t decompressed_size = 0;
int32_t image_count = get_i32_le(compressed_bytes, &p);
// Calculate the total uncompressed size
for (int i = 0; i < image_count; i++) {
decompressed_size += get_i32_le(compressed_bytes, &p);
}
uint32_t struct_size = sizeof(cmp_t) + sizeof(uint8_t *) * image_count;
cmp_t *cmp = mem_temp_alloc(struct_size + decompressed_size);
cmp->len = image_count;
uint8_t *decompressed_bytes = ((uint8_t *)cmp) + struct_size;
// Rewind and load all offsets
p = 4;
uint32_t offset = 0;
for (int i = 0; i < image_count; i++) {
cmp->entries[i] = decompressed_bytes + offset;
offset += get_i32_le(compressed_bytes, &p);
}
lzss_decompress(compressed_bytes + p, decompressed_bytes);
mem_temp_free(compressed_bytes);
return cmp;
}
uint16_t image_get_texture(char *name) {
printf("load: %s\n", name);
uint32_t size;
uint8_t *bytes = file_load(name, &size);
image_t *image = image_load_from_bytes(bytes, false);
uint32_t texture_index = render_texture_create(image->width, image->height, image->pixels);
mem_temp_free(image);
mem_temp_free(bytes);
return texture_index;
}
uint16_t image_get_texture_semi_trans(char *name) {
printf("load: %s\n", name);
uint32_t size;
uint8_t *bytes = file_load(name, &size);
image_t *image = image_load_from_bytes(bytes, true);
uint32_t texture_index = render_texture_create(image->width, image->height, image->pixels);
mem_temp_free(image);
mem_temp_free(bytes);
return texture_index;
}
texture_list_t image_get_compressed_textures(char *name) {
cmp_t *cmp = image_load_compressed(name);
texture_list_t list = {.start = render_textures_len(), .len = cmp->len};
for (int i = 0; i < cmp->len; i++) {
int32_t width, height;
image_t *image = image_load_from_bytes(cmp->entries[i], false);
// char png_name[1024] = {0};
// sprintf(png_name, "%s.%d.png", name, i);
// stbi_write_png(png_name, image->width, image->height, 4, image->pixels, 0);
render_texture_create(image->width, image->height, image->pixels);
mem_temp_free(image);
}
mem_temp_free(cmp);
return list;
}
uint16_t texture_from_list(texture_list_t tl, uint16_t index) {
error_if(index >= tl.len, "Texture %d not in list of len %d", index, tl.len);
return tl.start + index;
}
void image_copy(image_t *src, image_t *dst, uint32_t sx, uint32_t sy, uint32_t sw, uint32_t sh, uint32_t dx, uint32_t dy) {
rgba_t *src_pixels = src->pixels + sy * src->width + sx;
rgba_t *dst_pixels = dst->pixels + dy * dst->width + dx;
for (uint32_t y = 0; y < sh; y++) {
for (uint32_t x = 0; x < sw; x++) {
*(dst_pixels++) = *(src_pixels++);
}
src_pixels += src->width - sw;
dst_pixels += dst->width - sw;
}
}

@ -0,0 +1,34 @@
#ifndef INIT_H
#define INIT_H
#include "../types.h"
typedef struct {
uint16_t start;
uint16_t len;
} texture_list_t;
#define texture_list_empty() ((texture_list_t){0, 0})
typedef struct {
uint32_t width;
uint32_t height;
rgba_t *pixels;
} image_t;
typedef struct {
uint32_t len;
uint8_t *entries[];
} cmp_t;
image_t *image_alloc(uint32_t width, uint32_t height);
void image_copy(image_t *src, image_t *dst, uint32_t sx, uint32_t sy, uint32_t sw, uint32_t sh, uint32_t dx, uint32_t dy);
image_t *image_load_from_bytes(uint8_t *bytes, bool transparent);
cmp_t *image_load_compressed(char *name);
uint16_t image_get_texture(char *name);
uint16_t image_get_texture_semi_trans(char *name);
texture_list_t image_get_compressed_textures(char *name);
uint16_t texture_from_list(texture_list_t tl, uint16_t index);
#endif

@ -0,0 +1,486 @@
#include <string.h>
#include "../input.h"
#include "../system.h"
#include "../utils.h"
#include "../mem.h"
#include "menu.h"
#include "ingame_menus.h"
#include "game.h"
#include "image.h"
#include "ui.h"
#include "race.h"
static void page_race_points_init(menu_t * menu);
static void page_championship_points_init(menu_t * menu);
static void page_hall_of_fame_init(menu_t * menu);
static texture_list_t pilot_portraits;
static menu_t *ingame_menu;
void ingame_menus_load() {
pilot_portraits = image_get_compressed_textures(def.pilots[g.pilot].portrait);
ingame_menu = mem_bump(sizeof(menu_t));
}
// -----------------------------------------------------------------------------
// Pause Menu
static void button_continue(menu_t *menu, int data) {
race_unpause();
}
static void button_restart_confirm(menu_t *menu, int data) {
if (data) {
race_restart();
}
else {
menu_pop(menu);
}
}
static void button_restart_or_quit(menu_t *menu, int data) {
if (data) {
race_restart();
}
else {
game_set_scene(GAME_SCENE_MAIN_MENU);
}
}
static void button_restart(menu_t *menu, int data) {
menu_confirm(menu, "ARE YOU SURE YOU", "WANT TO RESTART", "YES", "NO", button_restart_confirm);
}
static void button_quit_confirm(menu_t *menu, int data) {
if (data) {
game_set_scene(GAME_SCENE_MAIN_MENU);
}
else {
menu_pop(menu);
}
}
static void button_quit(menu_t *menu, int data) {
menu_confirm(menu, "ARE YOU SURE YOU", "WANT TO QUIT", "YES", "NO", button_quit_confirm);
}
static void button_music_track(menu_t *menu, int data) {
sfx_music_play(data);
sfx_music_mode(SFX_MUSIC_LOOP);
}
static void button_music_random(menu_t *menu, int data) {
sfx_music_play(rand_int(0, len(def.music)));
sfx_music_mode(SFX_MUSIC_RANDOM);
}
static void button_music(menu_t *menu, int data) {
menu_page_t *page = menu_push(menu, "MUSIC", NULL);
for (int i = 0; i < len(def.music); i++) {
menu_page_add_button(page, i, def.music[i].name, button_music_track);
}
menu_page_add_button(page, 0, "RANDOM", button_music_random);
}
menu_t *pause_menu_init() {
sfx_play(SFX_MENU_SELECT);
menu_reset(ingame_menu);
menu_page_t *page = menu_push(ingame_menu, "PAUSED", NULL);
menu_page_add_button(page, 0, "CONTINUE", button_continue);
menu_page_add_button(page, 0, "RESTART", button_restart);
menu_page_add_button(page, 0, "QUIT", button_quit);
menu_page_add_button(page, 0, "MUSIC", button_music);
return ingame_menu;
}
// -----------------------------------------------------------------------------
// Game Over
menu_t *game_over_menu_init() {
sfx_play(SFX_MENU_SELECT);
menu_reset(ingame_menu);
menu_page_t *page = menu_push(ingame_menu, "GAME OVER", NULL);
menu_page_add_button(page, 1, "", button_quit_confirm);
return ingame_menu;
}
// -----------------------------------------------------------------------------
// Race Stats
static void button_qualify_confirm(menu_t *menu, int data) {
if (data) {
race_restart();
}
else {
game_set_scene(GAME_SCENE_MAIN_MENU);
}
}
static void button_race_stats_continue(menu_t *menu, int data) {
if (g.race_type == RACE_TYPE_CHAMPIONSHIP) {
if (g.race_position <= QUALIFYING_RANK) {
page_race_points_init(menu);
}
else {
menu_page_t *page = menu_confirm(menu, "CONTINUE QUALIFYING OR QUIT", "", "QUALIFY", "QUIT", button_qualify_confirm);
page->index = 0;
}
}
else {
if (g.is_new_race_record) {
page_hall_of_fame_init(menu);
}
else {
menu_confirm(menu, "", "RESTART RACE", "RESTART", "QUIT", button_restart_or_quit);
}
}
}
static void page_race_stats_draw(menu_t *menu, int data) {
menu_page_t *page = &menu->pages[menu->index];
vec2i_t pos = page->title_pos;
pos.x -= 140;
pos.y += 32;
ui_pos_t anchor = UI_POS_MIDDLE | UI_POS_CENTER;
// Pilot portrait and race position - only for championship or single race
if (g.race_type != RACE_TYPE_TIME_TRIAL) {
vec2i_t image_pos = ui_scaled_pos(anchor, vec2i(pos.x + 180, pos.y));
uint16_t image = texture_from_list(pilot_portraits, g.race_position <= QUALIFYING_RANK ? 1 : 0);
render_push_2d(image_pos, ui_scaled(render_texture_size(image)), rgba(0, 0, 0, 128), RENDER_NO_TEXTURE);
ui_draw_image(image_pos, image);
ui_draw_text("RACE POSITION", ui_scaled_pos(anchor, pos), UI_SIZE_8, UI_COLOR_ACCENT);
ui_draw_number(g.race_position, ui_scaled_pos(anchor, vec2i(pos.x + ui_text_width("RACE POSITION", UI_SIZE_8)+8, pos.y)), UI_SIZE_8, UI_COLOR_DEFAULT);
}
pos.y += 32;
ui_draw_text("RACE STATISTICS", ui_scaled_pos(anchor, pos), UI_SIZE_8, UI_COLOR_ACCENT);
pos.y += 16;
for (int i = 0; i < NUM_LAPS; i++) {
ui_draw_text("LAP", ui_scaled_pos(anchor, vec2i(pos.x + 8, pos.y)), UI_SIZE_8, UI_COLOR_ACCENT);
ui_draw_number(i+1, ui_scaled_pos(anchor, vec2i(pos.x + 50, pos.y)), UI_SIZE_8, UI_COLOR_ACCENT);
ui_draw_time(g.lap_times[g.pilot][i], ui_scaled_pos(anchor, vec2i(pos.x + 72, pos.y)), UI_SIZE_8, UI_COLOR_DEFAULT);
pos.y+= 12;
}
pos.y += 32;
ui_draw_text("RACE TIME", ui_scaled_pos(anchor, pos), UI_SIZE_8, UI_COLOR_ACCENT);
pos.y += 12;
ui_draw_time(g.race_time, ui_scaled_pos(anchor, vec2i(pos.x + 8, pos.y)), UI_SIZE_8, UI_COLOR_DEFAULT);
pos.y += 12;
ui_draw_text("BEST LAP", ui_scaled_pos(anchor, pos), UI_SIZE_8, UI_COLOR_ACCENT);
pos.y += 12;
ui_draw_time(g.best_lap, ui_scaled_pos(anchor, vec2i(pos.x + 8, pos.y)), UI_SIZE_8, UI_COLOR_DEFAULT);
pos.y += 12;
}
menu_t *race_stats_menu_init() {
sfx_play(SFX_MENU_SELECT);
menu_reset(ingame_menu);
char *title;
if (g.race_type == RACE_TYPE_TIME_TRIAL) {
title = "";
}
else if (g.race_position <= QUALIFYING_RANK) {
title = "CONGRATULATIONS";
}
else {
title = "FAILED TO QUALIFY";
}
menu_page_t *page = menu_push(ingame_menu, title, page_race_stats_draw);
flags_add(page->layout_flags, MENU_FIXED);
page->title_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
page->title_pos = vec2i(0, -100);
menu_page_add_button(page, 1, "", button_race_stats_continue);
return ingame_menu;
}
// -----------------------------------------------------------------------------
// Race Table
static void button_race_points_continue(menu_t *menu, int data) {
if (g.race_type == RACE_TYPE_CHAMPIONSHIP) {
page_championship_points_init(menu);
}
else if (g.is_new_race_record) {
page_hall_of_fame_init(menu);
}
else {
menu_confirm(menu, "", "RESTART RACE", "RESTART", "QUIT", button_restart_or_quit);
}
}
static void page_race_points_draw(menu_t *menu, int data) {
menu_page_t *page = &menu->pages[menu->index];
vec2i_t pos = page->title_pos;
pos.x -= 140;
pos.y += 32;
ui_pos_t anchor = UI_POS_MIDDLE | UI_POS_CENTER;
ui_draw_text("PILOT NAME", ui_scaled_pos(anchor, pos), UI_SIZE_8, UI_COLOR_ACCENT);
ui_draw_text("POINTS", ui_scaled_pos(anchor, vec2i(pos.x + 222, pos.y)), UI_SIZE_8, UI_COLOR_ACCENT);
pos.y += 24;
for (int i = 0; i < len(g.race_ranks); i++) {
rgba_t color = g.race_ranks[i].pilot == g.pilot ? UI_COLOR_ACCENT : UI_COLOR_DEFAULT;
ui_draw_text(def.pilots[g.race_ranks[i].pilot].name, ui_scaled_pos(anchor, pos), UI_SIZE_8, color);
int w = ui_number_width(g.race_ranks[i].points, UI_SIZE_8);
ui_draw_number(g.race_ranks[i].points, ui_scaled_pos(anchor, vec2i(pos.x + 280 - w, pos.y)), UI_SIZE_8, color);
pos.y += 12;
}
}
static void page_race_points_init(menu_t *menu) {
menu_page_t *page = menu_push(menu, "RACE POINTS", page_race_points_draw);
flags_add(page->layout_flags, MENU_FIXED);
page->title_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
page->title_pos = vec2i(0, -100);
menu_page_add_button(page, 1, "", button_race_points_continue);
}
// -----------------------------------------------------------------------------
// Championship Table
static void button_championship_points_continue(menu_t *menu, int data) {
if (g.is_new_race_record) {
page_hall_of_fame_init(menu);
}
else if (g.race_type == RACE_TYPE_CHAMPIONSHIP) {
race_next();
}
else {
menu_confirm(menu, "", "RESTART RACE", "RESTART", "QUIT", button_quit_confirm);
}
}
static void page_championship_points_draw(menu_t *menu, int data) {
menu_page_t *page = &menu->pages[menu->index];
vec2i_t pos = page->title_pos;
pos.x -= 140;
pos.y += 32;
ui_pos_t anchor = UI_POS_MIDDLE | UI_POS_CENTER;
ui_draw_text("PILOT NAME", ui_scaled_pos(anchor, pos), UI_SIZE_8, UI_COLOR_ACCENT);
ui_draw_text("POINTS", ui_scaled_pos(anchor, vec2i(pos.x + 222, pos.y)), UI_SIZE_8, UI_COLOR_ACCENT);
pos.y += 24;
for (int i = 0; i < len(g.championship_ranks); i++) {
rgba_t color = g.championship_ranks[i].pilot == g.pilot ? UI_COLOR_ACCENT : UI_COLOR_DEFAULT;
ui_draw_text(def.pilots[g.championship_ranks[i].pilot].name, ui_scaled_pos(anchor, pos), UI_SIZE_8, color);
int w = ui_number_width(g.championship_ranks[i].points, UI_SIZE_8);
ui_draw_number(g.championship_ranks[i].points, ui_scaled_pos(anchor, vec2i(pos.x + 280 - w, pos.y)), UI_SIZE_8, color);
pos.y += 12;
}
}
static void page_championship_points_init(menu_t *menu) {
menu_page_t *page = menu_push(menu, "CHAMPIONSHIP TABLE", page_championship_points_draw);
flags_add(page->layout_flags, MENU_FIXED);
page->title_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
page->title_pos = vec2i(0, -100);
menu_page_add_button(page, 1, "", button_championship_points_continue);
}
// -----------------------------------------------------------------------------
// Hall of Fame
static highscores_entry_t hs_new_entry = {
.time = 0,
.name = ""
};
static const char *hs_charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
static int hs_char_index = 0;
static bool hs_entry_complete = false;
static void hall_of_fame_draw_name_entry(menu_t *menu, ui_pos_t anchor, vec2i_t pos) {
int entry_len = strlen(hs_new_entry.name);
int entry_width = ui_text_width(hs_new_entry.name, UI_SIZE_16);
vec2i_t c_pos = ui_scaled_pos(anchor, vec2i(pos.x + entry_width, pos.y));
int c_first = 0;
int c_last = 38;
if (entry_len == 0) {
c_last = 37;
}
else if (entry_len == 3) {
c_first = 36;
}
if (input_pressed(A_MENU_UP)) {
hs_char_index++;
}
else if (input_pressed(A_MENU_DOWN)) {
hs_char_index--;
}
if (hs_char_index < c_first) {
hs_char_index = c_last-1;
}
if (hs_char_index >= c_last) {
hs_char_index = c_first;
}
// DEL
if (hs_char_index == 36) {
ui_draw_icon(UI_ICON_DEL, c_pos, UI_COLOR_ACCENT);
if (input_pressed(A_MENU_SELECT)) {
sfx_play(SFX_MENU_SELECT);
if (entry_len > 0) {
hs_new_entry.name[entry_len-1] = '\0';
}
}
}
// END
else if (hs_char_index == 37) {
ui_draw_icon(UI_ICON_END, c_pos, UI_COLOR_ACCENT);
if (input_pressed(A_MENU_SELECT)) {
hs_entry_complete = true;
}
}
// A-Z, 0-9
else {
char selector[2] = {hs_charset[hs_char_index], '\0'};
ui_draw_text(selector, c_pos, UI_SIZE_16, UI_COLOR_ACCENT);
if (input_pressed(A_MENU_SELECT)) {
sfx_play(SFX_MENU_SELECT);
hs_new_entry.name[entry_len] = hs_charset[hs_char_index];
hs_new_entry.name[entry_len+1] = '\0';
}
}
ui_draw_text(hs_new_entry.name, ui_scaled_pos(anchor, pos), UI_SIZE_16, UI_COLOR_ACCENT);
}
static void page_hall_of_fame_draw(menu_t *menu, int data) {
// FIXME: doing this all in the draw() function leads to all kinds of
// complications
highscores_t *hs = &save.highscores[g.race_class][g.circut][g.highscore_tab];
if (hs_entry_complete) {
sfx_play(SFX_MENU_SELECT);
strncpy(save.highscores_name, hs_new_entry.name, 4);
save.is_dirty = true;
// Insert new highscore entry into the save struct
highscores_entry_t temp_entry = hs->entries[0];
for (int i = 0; i < NUM_HIGHSCORES; i++) {
if (hs_new_entry.time < hs->entries[i].time) {
for (int j = NUM_HIGHSCORES - 2; j >= i; j--) {
hs->entries[j+1] = hs->entries[j];
}
hs->entries[i] = hs_new_entry;
break;
}
}
save.is_dirty = true;
if (g.race_type == RACE_TYPE_CHAMPIONSHIP) {
race_next();
}
else {
menu_reset(menu); // Can't go back!
menu_confirm(menu, "", "RESTART RACE", "RESTART", "QUIT", button_restart_or_quit);
}
return;
}
menu_page_t *page = &menu->pages[menu->index];
vec2i_t pos = page->title_pos;
pos.x -= 120;
pos.y += 48;
ui_pos_t anchor = UI_POS_MIDDLE | UI_POS_CENTER;
bool has_shown_new_entry = false;
for (int i = 0, j = 0; i < NUM_HIGHSCORES; i++, j++) {
if (!has_shown_new_entry && hs_new_entry.time < hs->entries[i].time) {
hall_of_fame_draw_name_entry(menu, anchor, pos);
ui_draw_time(hs_new_entry.time, ui_scaled_pos(anchor, vec2i(pos.x + 120, pos.y)), UI_SIZE_16, UI_COLOR_DEFAULT);
has_shown_new_entry = true;
j--;
}
else {
ui_draw_text(hs->entries[j].name, ui_scaled_pos(anchor, pos), UI_SIZE_16, UI_COLOR_DEFAULT);
ui_draw_time(hs->entries[j].time, ui_scaled_pos(anchor, vec2i(pos.x + 120, pos.y)), UI_SIZE_16, UI_COLOR_DEFAULT);
}
pos.y += 24;
}
}
static void page_hall_of_fame_init(menu_t *menu) {
menu_reset(menu); // Can't go back!
menu_page_t *page = menu_push(menu, "HALL OF FAME", page_hall_of_fame_draw);
flags_add(page->layout_flags, MENU_FIXED);
page->title_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
page->title_pos = vec2i(0, -100);
hs_new_entry.time = g.race_time;
strncpy(hs_new_entry.name, save.highscores_name, 4);
hs_char_index = 0;
hs_entry_complete = false;
}
// -----------------------------------------------------------------------------
// Text scroller
static char * const *text_scroll_lines;
static int text_scroll_lines_len;
static double text_scroll_start_time;
static void text_scroll_menu_draw(menu_t *menu, int data) {
double time = system_time() - text_scroll_start_time;
int scale = ui_get_scale();
int speed = 32;
vec2i_t screen = render_size();
vec2i_t pos = vec2i(screen.x / 2, screen.y - time * scale * speed);
for (int i = 0; i < text_scroll_lines_len; i++) {
const char *line = text_scroll_lines[i];
if (line[0] == '#') {
pos.y += 48 * scale;
ui_draw_text_centered(line + 1, pos, UI_SIZE_16, UI_COLOR_ACCENT);
pos.y += 32 * scale;
}
else {
ui_draw_text_centered(line, pos, UI_SIZE_8, UI_COLOR_DEFAULT);
pos.y += 12 * scale;
}
}
}
menu_t *text_scroll_menu_init(char * const *lines, int len) {
text_scroll_lines = lines;
text_scroll_lines_len = len;
text_scroll_start_time = system_time();
menu_reset(ingame_menu);
menu_page_t *page = menu_push(ingame_menu, "", text_scroll_menu_draw);
menu_page_add_button(page, 1, "", button_quit_confirm);
return ingame_menu;
}

@ -0,0 +1,13 @@
#ifndef PAUSE_MENU_H
#define PAUSE_MENU_H
#include "menu.h"
void ingame_menus_load();
menu_t *pause_menu_init();
menu_t *game_over_menu_init();
menu_t *race_stats_menu_init();
menu_t *text_scroll_menu_init(char * const *lines, int len);
#endif

@ -0,0 +1,105 @@
#include "../system.h"
#include "../input.h"
#include "../utils.h"
#include "../types.h"
#include "../mem.h"
#include "intro.h"
#include "ui.h"
#include "image.h"
#include "game.h"
void free_dummmy(void *p) {}
void *realloc_dummmy(void *p, size_t sz) {
die("pl_mpeg needed to realloc. Not implemented. Maybe increase PLM_BUFFER_DEFAULT_SIZE");
return NULL;
}
#define PL_MPEG_IMPLEMENTATION
#define PLM_MALLOC mem_bump
#define PLM_FREE free_dummmy
#define PLM_REALLOC realloc_dummmy
#include "../libs/pl_mpeg.h"
#define INTRO_AUDIO_BUFFER_LEN (64 * 1024)
static plm_t *plm;
static rgba_t *frame_buffer;
static int16_t texture;
static float *audio_buffer;
static int audio_buffer_read_pos;
static int audio_buffer_write_pos;
static void video_cb(plm_t *plm, plm_frame_t *frame, void *user);
static void audio_cb(plm_t *plm, plm_samples_t *samples, void *user);
static void audio_mix(float *samples, uint32_t len);
static void intro_end();
void intro_init() {
plm = plm_create_with_filename("wipeout/intro.mpeg");
if (!plm) {
intro_end();
return;
}
plm_set_video_decode_callback(plm, video_cb, NULL);
plm_set_audio_decode_callback(plm, audio_cb, NULL);
plm_set_loop(plm, false);
plm_set_audio_enabled(plm, true);
plm_set_audio_stream(plm, 0);
int w = plm_get_width(plm);
int h = plm_get_height(plm);
frame_buffer = mem_bump(w * h * sizeof(rgba_t));
for (int i = 0; i < w * h; i++) {
frame_buffer[i] = rgba(0, 0, 0, 255);
}
texture = render_texture_create(w, h, frame_buffer);
sfx_set_external_mix_cb(audio_mix);
audio_buffer = mem_bump(INTRO_AUDIO_BUFFER_LEN * sizeof(float) * 2);
audio_buffer_read_pos = 0;
audio_buffer_write_pos = 0;
}
static void intro_end() {
sfx_set_external_mix_cb(NULL);
game_set_scene(GAME_SCENE_TITLE);
}
void intro_update() {
if (!plm) {
return;
}
plm_decode(plm, system_tick());
render_set_view_2d();
render_push_2d(vec2i(0,0), render_size(), rgba(128, 128, 128, 255), texture);
if (plm_has_ended(plm) || input_pressed(A_MENU_SELECT) || input_pressed(A_MENU_START)) {
intro_end();
}
}
static void audio_cb(plm_t *plm, plm_samples_t *samples, void *user) {
int len = samples->count * 2;
for (int i = 0; i < len; i++) {
audio_buffer[audio_buffer_write_pos % INTRO_AUDIO_BUFFER_LEN] = samples->interleaved[i];
audio_buffer_write_pos++;
}
}
static void audio_mix(float *samples, uint32_t len) {
int i;
for (i = 0; i < len && audio_buffer_read_pos < audio_buffer_write_pos; i++) {
samples[i] = audio_buffer[audio_buffer_read_pos % INTRO_AUDIO_BUFFER_LEN];
audio_buffer_read_pos++;
}
for (; i < len; i++) {
samples[i] = 0;
}
}
static void video_cb(plm_t *plm, plm_frame_t *frame, void *user) {
plm_frame_to_rgba(frame, (uint8_t *)frame_buffer, plm_get_width(plm) * sizeof(rgba_t));
render_texture_replace_pixels(texture, frame_buffer);
}

@ -0,0 +1,8 @@
#ifndef INTRO_H
#define INTRO_H
void intro_init();
void intro_update();
void intro_cleanup();
#endif

@ -0,0 +1,541 @@
#include "../utils.h"
#include "../system.h"
#include "../mem.h"
#include "../platform.h"
#include "../input.h"
#include "menu.h"
#include "main_menu.h"
#include "game.h"
#include "image.h"
#include "ui.h"
static void page_main_init(menu_t *menu);
static void page_options_init(menu_t *menu);
static void page_race_class_init(menu_t *menu);
static void page_race_type_init(menu_t *menu);
static void page_team_init(menu_t *menu);
static void page_pilot_init(menu_t *menu);
static void page_circut_init(menu_t *menu);
static void page_options_controls_init(menu_t *menu);
static void page_options_video_init(menu_t *menu);
static void page_options_audio_init(menu_t *menu);
static uint16_t background;
static texture_list_t track_images;
static menu_t *main_menu;
static struct {
Object *race_classes[2];
Object *teams[4];
Object *pilots[8];
struct { Object *stopwatch, *save, *load, *headphones, *cd; } options;
struct { Object *championship, *msdos, *single_race, *options; } misc;
Object *rescue;
Object *controller;
} models;
static void draw_model(Object *model, vec2_t offset, vec3_t pos, float rotation) {
render_set_view(vec3(0,0,0), vec3(0, -M_PI, -M_PI));
render_set_screen_position(offset);
mat4_t mat = mat4_identity();
mat4_set_translation(&mat, pos);
mat4_set_yaw_pitch_roll(&mat, vec3(0, rotation, M_PI));
object_draw(model, &mat);
render_set_screen_position(vec2(0, 0));
}
// -----------------------------------------------------------------------------
// Main Menu
static void button_start_game(menu_t *menu, int data) {
page_race_class_init(menu);
}
static void button_options(menu_t *menu, int data) {
page_options_init(menu);
}
static void button_quit_confirm(menu_t *menu, int data) {
if (data) {
system_exit();
}
else {
menu_pop(menu);
}
}
static void button_quit(menu_t *menu, int data) {
menu_confirm(menu, "ARE YOU SURE YOU", "WANT TO QUIT", "YES", "NO", button_quit_confirm);
}
static void page_main_draw(menu_t *menu, int data) {
switch (data) {
case 0: draw_model(g.ships[0].model, vec2(0, -0.1), vec3(0, 0, -700), system_cycle_time()); break;
case 1: draw_model(models.misc.options, vec2(0, -0.2), vec3(0, 0, -700), system_cycle_time()); break;
case 2: draw_model(models.misc.msdos, vec2(0, -0.2), vec3(0, 0, -700), system_cycle_time()); break;
}
}
static void page_main_init(menu_t *menu) {
menu_page_t *page = menu_push(menu, "OPTIONS", page_main_draw);
flags_add(page->layout_flags, MENU_FIXED);
page->title_pos = vec2i(0, 30);
page->title_anchor = UI_POS_TOP | UI_POS_CENTER;
page->items_pos = vec2i(0, -110);
page->items_anchor = UI_POS_BOTTOM | UI_POS_CENTER;
menu_page_add_button(page, 0, "START GAME", button_start_game);
menu_page_add_button(page, 1, "OPTIONS", button_options);
#ifndef __EMSCRIPTEN__
menu_page_add_button(page, 2, "QUIT", button_quit);
#endif
}
// -----------------------------------------------------------------------------
// Options
static void button_controls(menu_t *menu, int data) {
page_options_controls_init(menu);
}
static void button_video(menu_t *menu, int data) {
page_options_video_init(menu);
}
static void button_audio(menu_t *menu, int data) {
page_options_audio_init(menu);
}
static void page_options_draw(menu_t *menu, int data) {
switch (data) {
case 0: draw_model(models.controller, vec2(0, -0.1), vec3(0, 0, -6000), system_cycle_time()); break;
case 1: draw_model(models.rescue, vec2(0, -0.2), vec3(0, 0, -700), system_cycle_time()); break; // TODO: needs better model
case 2: draw_model(models.options.headphones, vec2(0, -0.2), vec3(0, 0, -300), system_cycle_time()); break;
}
}
static void page_options_init(menu_t *menu) {
menu_page_t *page = menu_push(menu, "OPTIONS", page_options_draw);
flags_add(page->layout_flags, MENU_FIXED);
page->title_pos = vec2i(0, 30);
page->title_anchor = UI_POS_TOP | UI_POS_CENTER;
page->items_pos = vec2i(0, -110);
page->items_anchor = UI_POS_BOTTOM | UI_POS_CENTER;
menu_page_add_button(page, 0, "CONTROLS", button_controls);
menu_page_add_button(page, 1, "VIDEO", button_video);
menu_page_add_button(page, 2, "AUDIO", button_audio);
}
// -----------------------------------------------------------------------------
// Options Controls
static const char *button_names[NUM_GAME_ACTIONS][2] = {};
static int control_current_action;
static float await_input_deadline;
void button_capture(void *user, button_t button, int32_t ascii_char) {
if (button == INPUT_INVALID) {
return;
}
menu_t *menu = (menu_t *)user;
if (button == INPUT_KEY_ESCAPE) {
input_capture(NULL, NULL);
menu_pop(menu);
return;
}
int index = button < INPUT_KEY_MAX ? 0 : 1; // joypad or keyboard
// unbind this button if it's bound anywhere
for (int i = 0; i < len(save.buttons); i++) {
if (save.buttons[i][index] == button) {
save.buttons[i][index] = INPUT_INVALID;
}
}
input_capture(NULL, NULL);
input_bind(INPUT_LAYER_USER, button, control_current_action);
save.buttons[control_current_action][index] = button;
save.is_dirty = true;
menu_pop(menu);
}
static void page_options_control_set_draw(menu_t *menu, int data) {
float remaining = await_input_deadline - platform_now();
menu_page_t *page = &menu->pages[menu->index];
char remaining_text[2] = { '0' + (uint8_t)clamp(remaining + 1, 0, 3), '\0'};
vec2i_t pos = vec2i(page->items_pos.x, page->items_pos.y + 24);
ui_draw_text_centered(remaining_text, ui_scaled_pos(page->items_anchor, pos), UI_SIZE_16, UI_COLOR_DEFAULT);
if (remaining <= 0) {
input_capture(NULL, NULL);
menu_pop(menu);
return;
}
}
static void page_options_controls_set_init(menu_t *menu, int data) {
control_current_action = data;
await_input_deadline = platform_now() + 3;
menu_page_t *page = menu_push(menu, "AWAITING INPUT", page_options_control_set_draw);
input_capture(button_capture, menu);
}
static void page_options_control_draw(menu_t *menu, int data) {
menu_page_t *page = &menu->pages[menu->index];
int left = page->items_pos.x + page->block_width - 100;
int right = page->items_pos.x + page->block_width;
int line_y = page->items_pos.y - 20;
vec2i_t left_head_pos = vec2i(left - ui_text_width("KEYBOARD", UI_SIZE_8), line_y);
ui_draw_text("KEYBOARD", ui_scaled_pos(page->items_anchor, left_head_pos), UI_SIZE_8, UI_COLOR_DEFAULT);
vec2i_t right_head_pos = vec2i(right - ui_text_width("JOYSTICK", UI_SIZE_8), line_y);
ui_draw_text("JOYSTICK", ui_scaled_pos(page->items_anchor, right_head_pos), UI_SIZE_8, UI_COLOR_DEFAULT);
line_y += 20;
for (int action = 0; action < NUM_GAME_ACTIONS; action++) {
rgba_t text_color = UI_COLOR_DEFAULT;
if (action == data) {
text_color = UI_COLOR_ACCENT;
}
if (save.buttons[action][0] != INPUT_INVALID) {
const char *name = input_button_to_name(save.buttons[action][0]);
if (!name) {
name = "UNKNWN";
}
vec2i_t pos = vec2i(left - ui_text_width(name, UI_SIZE_8), line_y);
ui_draw_text(name, ui_scaled_pos(page->items_anchor, pos), UI_SIZE_8, text_color);
}
if (save.buttons[action][1] != INPUT_INVALID) {
const char *name = input_button_to_name(save.buttons[action][1]);
if (!name) {
name = "UNKNWN";
}
vec2i_t pos = vec2i(right - ui_text_width(name, UI_SIZE_8), line_y);
ui_draw_text(name, ui_scaled_pos(page->items_anchor, pos), UI_SIZE_8, text_color);
}
line_y += 12;
}
}
static void page_options_controls_init(menu_t *menu) {
menu_page_t *page = menu_push(menu, "CONTROLS", page_options_control_draw);
flags_set(page->layout_flags, MENU_VERTICAL | MENU_FIXED);
page->title_pos = vec2i(-160, -100);
page->title_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
page->items_pos = vec2i(-160, -50);
page->block_width = 320;
page->items_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
// const char *thrust_name = button_name(A_THRUST);
// printf("thrust: %s\n", thrust_name);
menu_page_add_button(page, A_UP, "UP", page_options_controls_set_init);
menu_page_add_button(page, A_DOWN, "DOWN", page_options_controls_set_init);
menu_page_add_button(page, A_LEFT, "LEFT", page_options_controls_set_init);
menu_page_add_button(page, A_RIGHT, "RIGHT", page_options_controls_set_init);
menu_page_add_button(page, A_BRAKE_LEFT, "BRAKE L", page_options_controls_set_init);
menu_page_add_button(page, A_BRAKE_RIGHT, "BRAKE R", page_options_controls_set_init);
menu_page_add_button(page, A_THRUST, "THRUST", page_options_controls_set_init);
menu_page_add_button(page, A_FIRE, "FIRE", page_options_controls_set_init);
menu_page_add_button(page, A_CHANGE_VIEW, "VIEW", page_options_controls_set_init);
}
// -----------------------------------------------------------------------------
// Options Video
static void toggle_fullscreen(menu_t *menu, int data) {
save.fullscreen = data;
save.is_dirty = true;
platform_set_fullscreen(save.fullscreen);
}
static void toggle_show_fps(menu_t *menu, int data) {
save.show_fps = data;
save.is_dirty = true;
}
static void toggle_ui_scale(menu_t *menu, int data) {
save.ui_scale = data;
save.is_dirty = true;
}
static void toggle_res(menu_t *menu, int data) {
render_set_resolution(data);
save.screen_res = data;
save.is_dirty = true;
}
static void toggle_post(menu_t *menu, int data) {
render_set_post_effect(data);
save.post_effect = data;
save.is_dirty = true;
}
static const char *opts_off_on[] = {"OFF", "ON"};
static const char *opts_ui_sizes[] = {"AUTO", "1X", "2X", "3X", "4X"};
static const char *opts_res[] = {"NATIVE", "240P", "480P"};
static const char *opts_post[] = {"NONE", "CRT EFFECT"};
static void page_options_video_init(menu_t *menu) {
menu_page_t *page = menu_push(menu, "VIDEO OPTIONS", NULL);
flags_set(page->layout_flags, MENU_VERTICAL | MENU_FIXED);
page->title_pos = vec2i(-160, -100);
page->title_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
page->items_pos = vec2i(-160, -60);
page->block_width = 320;
page->items_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
#ifndef __EMSCRIPTEN__
menu_page_add_toggle(page, save.fullscreen, "FULLSCREEN", opts_off_on, len(opts_off_on), toggle_fullscreen);
#endif
menu_page_add_toggle(page, save.ui_scale, "UI SCALE", opts_ui_sizes, len(opts_ui_sizes), toggle_ui_scale);
menu_page_add_toggle(page, save.show_fps, "SHOW FPS", opts_off_on, len(opts_off_on), toggle_show_fps);
menu_page_add_toggle(page, save.screen_res, "SCREEN RESOLUTION", opts_res, len(opts_res), toggle_res);
menu_page_add_toggle(page, save.post_effect, "POST PROCESSING", opts_post, len(opts_post), toggle_post);
}
// -----------------------------------------------------------------------------
// Options Audio
static void toggle_music_volume(menu_t *menu, int data) {
save.music_volume = (float)data * 0.1;
save.is_dirty = true;
}
static void toggle_sfx_volume(menu_t *menu, int data) {
save.sfx_volume = (float)data * 0.1;
save.is_dirty = true;
}
static const char *opts_volume[] = {"0", "10", "20", "30", "40", "50", "60", "70", "80", "90", "100"};
static void page_options_audio_init(menu_t *menu) {
menu_page_t *page = menu_push(menu, "AUDIO OPTIONS", NULL);
flags_set(page->layout_flags, MENU_VERTICAL | MENU_FIXED);
page->title_pos = vec2i(-160, -100);
page->title_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
page->items_pos = vec2i(-160, -80);
page->block_width = 320;
page->items_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
menu_page_add_toggle(page, save.music_volume * 10, "MUSIC VOLUME", opts_volume, len(opts_volume), toggle_music_volume);
menu_page_add_toggle(page, save.sfx_volume * 10, "SOUND EFFECTS VOLUME", opts_volume, len(opts_volume), toggle_sfx_volume);
}
// -----------------------------------------------------------------------------
// Racing class
static void button_race_class_select(menu_t *menu, int data) {
if (!save.has_rapier_class && data == RACE_CLASS_RAPIER) {
return;
}
g.race_class = data;
page_race_type_init(menu);
}
static void page_race_class_draw(menu_t *menu, int data) {
menu_page_t *page = &menu->pages[menu->index];
flags_add(page->layout_flags, MENU_FIXED);
page->title_pos = vec2i(0, 30);
page->title_anchor = UI_POS_TOP | UI_POS_CENTER;
page->items_pos = vec2i(0, -110);
page->items_anchor = UI_POS_BOTTOM | UI_POS_CENTER;
draw_model(models.race_classes[data], vec2(0, -0.2), vec3(0, 0, -350), system_cycle_time());
if (!save.has_rapier_class && data == RACE_CLASS_RAPIER) {
render_set_view_2d();
vec2i_t pos = vec2i(page->items_pos.x, page->items_pos.y + 32);
ui_draw_text_centered("NOT AVAILABLE", ui_scaled_pos(page->items_anchor, pos), UI_SIZE_12, UI_COLOR_ACCENT);
}
}
static void page_race_class_init(menu_t *menu) {
menu_page_t *page = menu_push(menu, "SELECT RACING CLASS", page_race_class_draw);
for (int i = 0; i < len(def.race_classes); i++) {
menu_page_add_button(page, i, def.race_classes[i].name, button_race_class_select);
}
}
// -----------------------------------------------------------------------------
// Race Type
static void button_race_type_select(menu_t *menu, int data) {
g.race_type = data;
g.highscore_tab = g.race_type == RACE_TYPE_TIME_TRIAL ? HIGHSCORE_TAB_TIME_TRIAL : HIGHSCORE_TAB_RACE;
page_team_init(menu);
}
static void page_race_type_draw(menu_t *menu, int data) {
switch (data) {
case 0: draw_model(models.misc.championship, vec2(0, -0.2), vec3(0, 0, -400), system_cycle_time()); break;
case 1: draw_model(models.misc.single_race, vec2(0, -0.2), vec3(0, 0, -400), system_cycle_time()); break;
case 2: draw_model(models.options.stopwatch, vec2(0, -0.2), vec3(0, 0, -400), system_cycle_time()); break;
}
}
static void page_race_type_init(menu_t *menu) {
menu_page_t *page = menu_push(menu, "SELECT RACE TYPE", page_race_type_draw);
flags_add(page->layout_flags, MENU_FIXED);
page->title_pos = vec2i(0, 30);
page->title_anchor = UI_POS_TOP | UI_POS_CENTER;
page->items_pos = vec2i(0, -110);
page->items_anchor = UI_POS_BOTTOM | UI_POS_CENTER;
for (int i = 0; i < len(def.race_types); i++) {
menu_page_add_button(page, i, def.race_types[i].name, button_race_type_select);
}
}
// -----------------------------------------------------------------------------
// Team
static void button_team_select(menu_t *menu, int data) {
g.team = data;
page_pilot_init(menu);
}
static void page_team_draw(menu_t *menu, int data) {
int team_model_index = (data + 3) % 4; // models in the prm are shifted by -1
draw_model(models.teams[team_model_index], vec2(0, -0.2), vec3(0, 0, -10000), system_cycle_time());
draw_model(g.ships[def.teams[data].pilots[0]].model, vec2(0, -0.3), vec3(-700, -800, -1300), system_cycle_time()*1.1);
draw_model(g.ships[def.teams[data].pilots[1]].model, vec2(0, -0.3), vec3( 700, -800, -1300), system_cycle_time()*1.2);
}
static void page_team_init(menu_t *menu) {
menu_page_t *page = menu_push(menu, "SELECT YOUR TEAM", page_team_draw);
flags_add(page->layout_flags, MENU_FIXED);
page->title_pos = vec2i(0, 30);
page->title_anchor = UI_POS_TOP | UI_POS_CENTER;
page->items_pos = vec2i(0, -110);
page->items_anchor = UI_POS_BOTTOM | UI_POS_CENTER;
for (int i = 0; i < len(def.teams); i++) {
menu_page_add_button(page, i, def.teams[i].name, button_team_select);
}
}
// -----------------------------------------------------------------------------
// Pilot
static void button_pilot_select(menu_t *menu, int data) {
g.pilot = data;
if (g.race_type != RACE_TYPE_CHAMPIONSHIP) {
page_circut_init(menu);
}
else {
g.circut = 0;
game_reset_championship();
game_set_scene(GAME_SCENE_RACE);
}
}
static void page_pilot_draw(menu_t *menu, int data) {
draw_model(models.pilots[data], vec2(0, -0.2), vec3(0, 0, -10000), system_cycle_time());
}
static void page_pilot_init(menu_t *menu) {
menu_page_t *page = menu_push(menu, "CHOOSE YOUR PILOT", page_pilot_draw);
flags_add(page->layout_flags, MENU_FIXED);
page->title_pos = vec2i(0, 30);
page->title_anchor = UI_POS_TOP | UI_POS_CENTER;
page->items_pos = vec2i(0, -110);
page->items_anchor = UI_POS_BOTTOM | UI_POS_CENTER;
for (int i = 0; i < len(def.teams[g.team].pilots); i++) {
menu_page_add_button(page, def.teams[g.team].pilots[i], def.pilots[def.teams[g.team].pilots[i]].name, button_pilot_select);
}
}
// -----------------------------------------------------------------------------
// Circut
static void button_circut_select(menu_t *menu, int data) {
g.circut = data;
game_set_scene(GAME_SCENE_RACE);
}
static void page_circut_draw(menu_t *menu, int data) {
vec2i_t pos = vec2i(0, -25);
vec2i_t size = vec2i(128, 74);
vec2i_t scaled_size = ui_scaled(size);
vec2i_t scaled_pos = ui_scaled_pos(UI_POS_MIDDLE | UI_POS_CENTER, vec2i(pos.x - size.x/2, pos.y - size.y/2));
render_push_2d(scaled_pos, scaled_size, rgba(128, 128, 128, 255), texture_from_list(track_images, data));
}
static void page_circut_init(menu_t *menu) {
menu_page_t *page = menu_push(menu, "SELECT RACING CIRCUT", page_circut_draw);
flags_add(page->layout_flags, MENU_FIXED);
page->title_pos = vec2i(0, 30);
page->title_anchor = UI_POS_TOP | UI_POS_CENTER;
page->items_pos = vec2i(0, -100);
page->items_anchor = UI_POS_BOTTOM | UI_POS_CENTER;
for (int i = 0; i < len(def.circuts); i++) {
if (!def.circuts[i].is_bonus_circut || save.has_bonus_circuts) {
menu_page_add_button(page, i, def.circuts[i].name, button_circut_select);
}
}
}
#define objects_unpack(DEST, SRC) \
objects_unpack_imp((Object **)&DEST, sizeof(DEST)/sizeof(Object*), SRC)
static void objects_unpack_imp(Object **dest_array, int len, Object *src) {
int i;
for (i = 0; src && i < len; i++) {
dest_array[i] = src;
src = src->next;
}
error_if(i != len, "expected %d models got %d", len, i)
}
void main_menu_init() {
g.is_attract_mode = false;
main_menu = mem_bump(sizeof(menu_t));
background = image_get_texture("wipeout/textures/wipeout1.tim");
track_images = image_get_compressed_textures("wipeout/textures/track.cmp");
objects_unpack(models.race_classes, objects_load("wipeout/common/leeg.prm", image_get_compressed_textures("wipeout/common/leeg.cmp")));
objects_unpack(models.teams, objects_load("wipeout/common/teams.prm", texture_list_empty()));
objects_unpack(models.pilots, objects_load("wipeout/common/pilot.prm", image_get_compressed_textures("wipeout/common/pilot.cmp")));
objects_unpack(models.options, objects_load("wipeout/common/alopt.prm", image_get_compressed_textures("wipeout/common/alopt.cmp")));
objects_unpack(models.rescue, objects_load("wipeout/common/rescu.prm", image_get_compressed_textures("wipeout/common/rescu.cmp")));
objects_unpack(models.controller, objects_load("wipeout/common/pad1.prm", image_get_compressed_textures("wipeout/common/pad1.cmp")));
objects_unpack(models.misc, objects_load("wipeout/common/msdos.prm", image_get_compressed_textures("wipeout/common/msdos.cmp")));
menu_reset(main_menu);
page_main_init(main_menu);
}
void main_menu_update() {
render_set_view_2d();
render_push_2d(vec2i(0, 0), render_size(), rgba(128, 128, 128, 255), background);
menu_update(main_menu);
}

@ -0,0 +1,7 @@
#ifndef MAIN_MENU_H
#define MAIN_MENU_H
void main_menu_init();
void main_menu_update();
#endif

@ -0,0 +1,245 @@
#include "../system.h"
#include "../input.h"
#include "../utils.h"
#include "game.h"
#include "menu.h"
#include "ui.h"
#include "sfx.h"
bool blink() {
// blink 30 times per second
return fmod(system_cycle_time(), 1.0/15.0) < 1.0/30.0;
}
void menu_reset(menu_t *menu) {
menu->index = -1;
}
menu_page_t *menu_push(menu_t *menu, char *title, void(*draw_func)(menu_t *, int)) {
error_if(menu->index >= MENU_PAGES_MAX-1, "MENU_PAGES_MAX exceeded");
menu_page_t *page = &menu->pages[++menu->index];
page->layout_flags = MENU_VERTICAL | MENU_ALIGN_CENTER;
page->block_width = 320;
page->title = title;
page->subtitle = NULL;
page->draw_func = draw_func;
page->entries_len = 0;
page->index = 0;
page->title_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
page->items_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
return page;
}
menu_page_t *menu_confirm(menu_t *menu, char *title, char *subtitle, char *yes, char *no, void(*confirm_func)(menu_t *, int)) {
error_if(menu->index >= MENU_PAGES_MAX-1, "MENU_PAGES_MAX exceeded");
menu_page_t *page = &menu->pages[++menu->index];
page->layout_flags = MENU_HORIZONTAL;
page->title = title;
page->subtitle = subtitle;
page->draw_func = NULL;
page->entries_len = 0;
menu_page_add_button(page, 1, yes, confirm_func);
menu_page_add_button(page, 0, no, confirm_func);
page->index = 1;
page->title_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
page->items_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
return page;
}
void menu_pop(menu_t *menu) {
if (menu->index == 0) {
return;
}
menu->index--;
}
void menu_page_add_button(menu_page_t *page, int data, char *text, void(*select_func)(menu_t *, int)) {
error_if(page->entries_len >= MENU_ENTRIES_MAX-1, "MENU_ENTRIES_MAX exceeded");
menu_entry_t *entry = &page->entries[page->entries_len++];
entry->data = data;
entry->text = text;
entry->select_func = select_func;
entry->type = MENU_ENTRY_BUTTON;
}
void menu_page_add_toggle(menu_page_t *page, int data, char *text, const char **options, int len, void(*select_func)(menu_t *, int)) {
error_if(page->entries_len >= MENU_ENTRIES_MAX-1, "MENU_ENTRIES_MAX exceeded");
menu_entry_t *entry = &page->entries[page->entries_len++];
entry->data = data;
entry->text = text;
entry->select_func = select_func;
entry->type = MENU_ENTRY_TOGGLE;
entry->options = options;
entry->options_len = len;
}
void menu_update(menu_t *menu) {
render_set_view_2d();
error_if(menu->index < 0, "Attempt to update menu without a page");
menu_page_t *page = &menu->pages[menu->index];
// Handle menu entry selecting
int last_index = page->index;
int selected_data = 0;
if (page->entries_len > 0) {
if (flags_is(page->layout_flags, MENU_HORIZONTAL)) {
if (input_pressed(A_MENU_LEFT)) {
page->index--;
}
else if (input_pressed(A_MENU_RIGHT)) {
page->index++;
}
}
else {
if (input_pressed(A_MENU_UP)) {
page->index--;
}
if (input_pressed(A_MENU_DOWN)) {
page->index++;
}
}
if (page->index >= page->entries_len) {
page->index = 0;
}
if (page->index < 0) {
page->index = page->entries_len - 1;
}
if (last_index != page->index) {
sfx_play(SFX_MENU_MOVE);
}
selected_data = page->entries[page->index].data;
}
if (page->draw_func) {
page->draw_func(menu, selected_data);
}
render_set_view_2d();
// Draw Horizontal (confirm)
if (flags_is(page->layout_flags, MENU_HORIZONTAL)) {
vec2i_t pos = vec2i(0, -20);
ui_draw_text_centered(page->title, ui_scaled_pos(page->title_anchor, pos), UI_SIZE_8, UI_COLOR_DEFAULT);
if (page->subtitle) {
pos.y += 12;
ui_draw_text_centered(page->subtitle, ui_scaled_pos(page->title_anchor, pos), UI_SIZE_8, UI_COLOR_DEFAULT);
}
pos.y += 16;
page = &menu->pages[menu->index];
pos.x = -50;
for (int i = 0; i < page->entries_len; i++) {
menu_entry_t *entry = &page->entries[i];
rgba_t text_color;
if (i == page->index && blink()) {
text_color = UI_COLOR_ACCENT;
}
else {
text_color = UI_COLOR_DEFAULT;
}
ui_draw_text_centered(entry->text, ui_scaled_pos(page->items_anchor, pos), UI_SIZE_16, text_color);
pos.x = 60;
}
}
// Draw Vertical
else {
vec2i_t title_pos, items_pos;
if (flags_not(page->layout_flags, MENU_FIXED)) {
int height = 20 + page->entries_len * 12;
title_pos = vec2i(0, -height/2);
items_pos = vec2i(0, -height/2 + 20);
}
else {
title_pos = page->title_pos;
items_pos = page->items_pos;
}
if (flags_is(page->layout_flags, MENU_ALIGN_CENTER)) {
ui_draw_text_centered(page->title, ui_scaled_pos(page->title_anchor, title_pos), UI_SIZE_12, UI_COLOR_ACCENT);
}
else {
ui_draw_text(page->title, ui_scaled_pos(page->title_anchor, title_pos), UI_SIZE_12, UI_COLOR_ACCENT);
}
page = &menu->pages[menu->index];
for (int i = 0; i < page->entries_len; i++) {
menu_entry_t *entry = &page->entries[i];
rgba_t text_color;
if (i == page->index && blink()) {
text_color = UI_COLOR_ACCENT;
}
else {
text_color = UI_COLOR_DEFAULT;
}
if (flags_is(page->layout_flags, MENU_ALIGN_CENTER)) {
ui_draw_text_centered(entry->text, ui_scaled_pos(page->items_anchor, items_pos), UI_SIZE_8, text_color);
}
else {
ui_draw_text(entry->text, ui_scaled_pos(page->items_anchor, items_pos), UI_SIZE_8, text_color);
}
if (entry->type == MENU_ENTRY_TOGGLE) {
vec2i_t toggle_pos = items_pos;
toggle_pos.x += page->block_width - ui_text_width(entry->options[entry->data], UI_SIZE_8);
ui_draw_text(entry->options[entry->data], ui_scaled_pos(page->items_anchor, toggle_pos), UI_SIZE_8, text_color);
}
items_pos.y += 12;
}
}
// Handle back buttons
if (input_pressed(A_MENU_BACK) || input_pressed(A_MENU_QUIT)) {
if (menu->index != 0) {
menu_pop(menu);
sfx_play(SFX_MENU_SELECT);
}
return;
}
if (page->entries_len == 0) {
return;
}
// Handle toggle entries
menu_entry_t *entry = &page->entries[page->index];
if (entry->type == MENU_ENTRY_TOGGLE) {
if (input_pressed(A_MENU_LEFT)) {
sfx_play(SFX_MENU_SELECT);
entry->data--;
if (entry->data < 0) {
entry->data = entry->options_len-1;
}
if (entry->select_func) {
entry->select_func(menu, entry->data);
}
}
else if (input_pressed(A_MENU_RIGHT) || input_pressed(A_MENU_SELECT) || input_pressed(A_MENU_START)) {
sfx_play(SFX_MENU_SELECT);
entry->data = (entry->data + 1) % entry->options_len;
if (entry->select_func) {
entry->select_func(menu, entry->data);
}
}
}
// Handle buttons
else {
if (input_pressed(A_MENU_SELECT) || input_pressed(A_MENU_START)) {
if (entry->select_func) {
sfx_play(SFX_MENU_SELECT);
if (entry->type == MENU_ENTRY_TOGGLE) {
entry->data = (entry->data + 1) % entry->options_len;
}
entry->select_func(menu, entry->data);
}
}
}
}

@ -0,0 +1,65 @@
#ifndef MENU_H
#define MENU_H
#include "../types.h"
#include "ui.h"
#define MENU_PAGES_MAX 8
#define MENU_ENTRIES_MAX 16
typedef enum {
MENU_ENTRY_BUTTON,
MENU_ENTRY_TOGGLE
} menu_entry_type_t;
typedef enum {
MENU_VERTICAL = (1<<0),
MENU_HORIZONTAL = (1<<1),
MENU_FIXED = (1<<2),
MENU_ALIGN_CENTER = (1<<3),
MENU_ALIGN_BLOCK = (1<<4)
} menu_page_layout_t;
typedef struct menu_t menu_t;
typedef struct menu_page_t menu_page_t;
typedef struct menu_entry_t menu_entry_t;
typedef struct menu_entry_options_t menu_entry_options_t;
struct menu_entry_t {
menu_entry_type_t type;
int data;
char *text;
void (*select_func)(menu_t *, int);
const char **options;
int options_len;
};
struct menu_page_t {
char *title, *subtitle;
menu_page_layout_t layout_flags;
void (*draw_func)(menu_t *, int);
menu_entry_t entries[MENU_ENTRIES_MAX];
int entries_len;
int index;
int block_width;
vec2i_t title_pos;
ui_pos_t title_anchor;
vec2i_t items_pos;
ui_pos_t items_anchor;
};
struct menu_t {
menu_page_t pages[MENU_PAGES_MAX];
int index;
};
void menu_reset(menu_t *menu);
menu_page_t *menu_push(menu_t *menu, char *title, void(*draw_func)(menu_t *, int));
menu_page_t *menu_confirm(menu_t *menu, char *title, char *subtitle, char *yes, char *no, void(*confirm_func)(menu_t *, int));
void menu_pop(menu_t *menu);
void menu_page_add_button(menu_page_t *page, int data, char *text, void(*select_func)(menu_t *, int));
void menu_page_add_toggle(menu_page_t *page, int data, char *text, const char **options, int len, void(*select_func)(menu_t *, int));
void menu_update(menu_t *menu);
#endif

@ -0,0 +1,778 @@
#include "../types.h"
#include "../mem.h"
#include "../render.h"
#include "../utils.h"
#include "object.h"
#include "track.h"
#include "ship.h"
#include "weapon.h"
#include "droid.h"
#include "camera.h"
#include "object.h"
#include "scene.h"
#include "hud.h"
#include "object.h"
static rgba_t int32_to_rgba(uint32_t v) {
return rgba(
((v >> 24) & 0xff),
((v >> 16) & 0xff),
((v >> 8) & 0xff),
255
);
}
Object *objects_load(char *name, texture_list_t tl) {
uint32_t length = 0;
uint8_t *bytes = file_load(name, &length);
if (!bytes) {
die("Failed to load file %s\n", name);
}
printf("load: %s\n", name);
Object *objectList = mem_mark();
Object *prevObject = NULL;
uint32_t p = 0;
while (p < length) {
Object *object = mem_bump(sizeof(Object));
if (prevObject) {
prevObject->next = object;
}
prevObject = object;
for (int i = 0; i < 16; i++) {
object->name[i] = get_i8(bytes, &p);
}
object->mat = mat4_identity();
object->vertices_len = get_i16(bytes, &p); p += 2;
object->vertices = NULL; get_i32(bytes, &p);
object->normals_len = get_i16(bytes, &p); p += 2;
object->normals = NULL; get_i32(bytes, &p);
object->primitives_len = get_i16(bytes, &p); p += 2;
object->primitives = NULL; get_i32(bytes, &p);
get_i32(bytes, &p);
get_i32(bytes, &p);
get_i32(bytes, &p); // Skeleton ref
object->extent = get_i32(bytes, &p);
object->flags = get_i16(bytes, &p); p += 2;
object->next = NULL; get_i32(bytes, &p);
p += 3 * 3 * 2; // relative rot matrix
p += 2; // padding
object->origin.x = get_i32(bytes, &p);
object->origin.y = get_i32(bytes, &p);
object->origin.z = get_i32(bytes, &p);
p += 3 * 3 * 2; // absolute rot matrix
p += 2; // padding
p += 3 * 4; // absolute translation matrix
p += 2; // skeleton update flag
p += 2; // padding
p += 4; // skeleton super
p += 4; // skeleton sub
p += 4; // skeleton next
object->vertices = mem_bump(object->vertices_len * sizeof(vec3_t));
for (int i = 0; i < object->vertices_len; i++) {
object->vertices[i].x = get_i16(bytes, &p);
object->vertices[i].y = get_i16(bytes, &p);
object->vertices[i].z = get_i16(bytes, &p);
p += 2; // padding
}
object->normals = mem_bump(object->normals_len * sizeof(vec3_t));
for (int i = 0; i < object->normals_len; i++) {
object->normals[i].x = get_i16(bytes, &p);
object->normals[i].y = get_i16(bytes, &p);
object->normals[i].z = get_i16(bytes, &p);
p += 2; // padding
}
object->primitives = mem_mark();
for (int i = 0; i < object->primitives_len; i++) {
Prm prm;
int16_t prm_type = get_i16(bytes, &p);
int16_t prm_flag = get_i16(bytes, &p);
switch (prm_type) {
case PRM_TYPE_F3:
prm.ptr = mem_bump(sizeof(F3));
prm.f3->coords[0] = get_i16(bytes, &p);
prm.f3->coords[1] = get_i16(bytes, &p);
prm.f3->coords[2] = get_i16(bytes, &p);
prm.f3->pad1 = get_i16(bytes, &p);
prm.f3->colour = int32_to_rgba(get_i32(bytes, &p));
break;
case PRM_TYPE_F4:
prm.ptr = mem_bump(sizeof(F4));
prm.f4->coords[0] = get_i16(bytes, &p);
prm.f4->coords[1] = get_i16(bytes, &p);
prm.f4->coords[2] = get_i16(bytes, &p);
prm.f4->coords[3] = get_i16(bytes, &p);
prm.f4->colour = int32_to_rgba(get_i32(bytes, &p));
break;
case PRM_TYPE_FT3:
prm.ptr = mem_bump(sizeof(FT3));
prm.ft3->coords[0] = get_i16(bytes, &p);
prm.ft3->coords[1] = get_i16(bytes, &p);
prm.ft3->coords[2] = get_i16(bytes, &p);
prm.ft3->texture = texture_from_list(tl, get_i16(bytes, &p));
prm.ft3->cba = get_i16(bytes, &p);
prm.ft3->tsb = get_i16(bytes, &p);
prm.ft3->u0 = get_i8(bytes, &p);
prm.ft3->v0 = get_i8(bytes, &p);
prm.ft3->u1 = get_i8(bytes, &p);
prm.ft3->v1 = get_i8(bytes, &p);
prm.ft3->u2 = get_i8(bytes, &p);
prm.ft3->v2 = get_i8(bytes, &p);
prm.ft3->pad1 = get_i16(bytes, &p);
prm.ft3->colour = int32_to_rgba(get_i32(bytes, &p));
break;
case PRM_TYPE_FT4:
prm.ptr = mem_bump(sizeof(FT4));
prm.ft4->coords[0] = get_i16(bytes, &p);
prm.ft4->coords[1] = get_i16(bytes, &p);
prm.ft4->coords[2] = get_i16(bytes, &p);
prm.ft4->coords[3] = get_i16(bytes, &p);
prm.ft4->texture = texture_from_list(tl, get_i16(bytes, &p));
prm.ft4->cba = get_i16(bytes, &p);
prm.ft4->tsb = get_i16(bytes, &p);
prm.ft4->u0 = get_i8(bytes, &p);
prm.ft4->v0 = get_i8(bytes, &p);
prm.ft4->u1 = get_i8(bytes, &p);
prm.ft4->v1 = get_i8(bytes, &p);
prm.ft4->u2 = get_i8(bytes, &p);
prm.ft4->v2 = get_i8(bytes, &p);
prm.ft4->u3 = get_i8(bytes, &p);
prm.ft4->v3 = get_i8(bytes, &p);
prm.ft4->pad1 = get_i16(bytes, &p);
prm.ft4->colour = int32_to_rgba(get_i32(bytes, &p));
break;
case PRM_TYPE_G3:
prm.ptr = mem_bump(sizeof(G3));
prm.g3->coords[0] = get_i16(bytes, &p);
prm.g3->coords[1] = get_i16(bytes, &p);
prm.g3->coords[2] = get_i16(bytes, &p);
prm.g3->pad1 = get_i16(bytes, &p);
prm.g3->colour[0] = int32_to_rgba(get_i32(bytes, &p));
prm.g3->colour[1] = int32_to_rgba(get_i32(bytes, &p));
prm.g3->colour[2] = int32_to_rgba(get_i32(bytes, &p));
break;
case PRM_TYPE_G4:
prm.ptr = mem_bump(sizeof(G4));
prm.g4->coords[0] = get_i16(bytes, &p);
prm.g4->coords[1] = get_i16(bytes, &p);
prm.g4->coords[2] = get_i16(bytes, &p);
prm.g4->coords[3] = get_i16(bytes, &p);
prm.g4->colour[0] = int32_to_rgba(get_i32(bytes, &p));
prm.g4->colour[1] = int32_to_rgba(get_i32(bytes, &p));
prm.g4->colour[2] = int32_to_rgba(get_i32(bytes, &p));
prm.g4->colour[3] = int32_to_rgba(get_i32(bytes, &p));
break;
case PRM_TYPE_GT3:
prm.ptr = mem_bump(sizeof(GT3));
prm.gt3->coords[0] = get_i16(bytes, &p);
prm.gt3->coords[1] = get_i16(bytes, &p);
prm.gt3->coords[2] = get_i16(bytes, &p);
prm.gt3->texture = texture_from_list(tl, get_i16(bytes, &p));
prm.gt3->cba = get_i16(bytes, &p);
prm.gt3->tsb = get_i16(bytes, &p);
prm.gt3->u0 = get_i8(bytes, &p);
prm.gt3->v0 = get_i8(bytes, &p);
prm.gt3->u1 = get_i8(bytes, &p);
prm.gt3->v1 = get_i8(bytes, &p);
prm.gt3->u2 = get_i8(bytes, &p);
prm.gt3->v2 = get_i8(bytes, &p);
prm.gt3->pad1 = get_i16(bytes, &p);
prm.gt3->colour[0] = int32_to_rgba(get_i32(bytes, &p));
prm.gt3->colour[1] = int32_to_rgba(get_i32(bytes, &p));
prm.gt3->colour[2] = int32_to_rgba(get_i32(bytes, &p));
break;
case PRM_TYPE_GT4:
prm.ptr = mem_bump(sizeof(GT4));
prm.gt4->coords[0] = get_i16(bytes, &p);
prm.gt4->coords[1] = get_i16(bytes, &p);
prm.gt4->coords[2] = get_i16(bytes, &p);
prm.gt4->coords[3] = get_i16(bytes, &p);
prm.gt4->texture = texture_from_list(tl, get_i16(bytes, &p));
prm.gt4->cba = get_i16(bytes, &p);
prm.gt4->tsb = get_i16(bytes, &p);
prm.gt4->u0 = get_i8(bytes, &p);
prm.gt4->v0 = get_i8(bytes, &p);
prm.gt4->u1 = get_i8(bytes, &p);
prm.gt4->v1 = get_i8(bytes, &p);
prm.gt4->u2 = get_i8(bytes, &p);
prm.gt4->v2 = get_i8(bytes, &p);
prm.gt4->u3 = get_i8(bytes, &p);
prm.gt4->v3 = get_i8(bytes, &p);
prm.gt4->pad1 = get_i16(bytes, &p);
prm.gt4->colour[0] = int32_to_rgba(get_i32(bytes, &p));
prm.gt4->colour[1] = int32_to_rgba(get_i32(bytes, &p));
prm.gt4->colour[2] = int32_to_rgba(get_i32(bytes, &p));
prm.gt4->colour[3] = int32_to_rgba(get_i32(bytes, &p));
break;
case PRM_TYPE_LSF3:
prm.ptr = mem_bump(sizeof(LSF3));
prm.lsf3->coords[0] = get_i16(bytes, &p);
prm.lsf3->coords[1] = get_i16(bytes, &p);
prm.lsf3->coords[2] = get_i16(bytes, &p);
prm.lsf3->normal = get_i16(bytes, &p);
prm.lsf3->colour = int32_to_rgba(get_i32(bytes, &p));
break;
case PRM_TYPE_LSF4:
prm.ptr = mem_bump(sizeof(LSF4));
prm.lsf4->coords[0] = get_i16(bytes, &p);
prm.lsf4->coords[1] = get_i16(bytes, &p);
prm.lsf4->coords[2] = get_i16(bytes, &p);
prm.lsf4->coords[3] = get_i16(bytes, &p);
prm.lsf4->normal = get_i16(bytes, &p);
prm.lsf4->pad1 = get_i16(bytes, &p);
prm.lsf4->colour = int32_to_rgba(get_i32(bytes, &p));
break;
case PRM_TYPE_LSFT3:
prm.ptr = mem_bump(sizeof(LSFT3));
prm.lsft3->coords[0] = get_i16(bytes, &p);
prm.lsft3->coords[1] = get_i16(bytes, &p);
prm.lsft3->coords[2] = get_i16(bytes, &p);
prm.lsft3->normal = get_i16(bytes, &p);
prm.lsft3->texture = texture_from_list(tl, get_i16(bytes, &p));
prm.lsft3->cba = get_i16(bytes, &p);
prm.lsft3->tsb = get_i16(bytes, &p);
prm.lsft3->u0 = get_i8(bytes, &p);
prm.lsft3->v0 = get_i8(bytes, &p);
prm.lsft3->u1 = get_i8(bytes, &p);
prm.lsft3->v1 = get_i8(bytes, &p);
prm.lsft3->u2 = get_i8(bytes, &p);
prm.lsft3->v2 = get_i8(bytes, &p);
prm.lsft3->colour = int32_to_rgba(get_i32(bytes, &p));
break;
case PRM_TYPE_LSFT4:
prm.ptr = mem_bump(sizeof(LSFT4));
prm.lsft4->coords[0] = get_i16(bytes, &p);
prm.lsft4->coords[1] = get_i16(bytes, &p);
prm.lsft4->coords[2] = get_i16(bytes, &p);
prm.lsft4->coords[3] = get_i16(bytes, &p);
prm.lsft4->normal = get_i16(bytes, &p);
prm.lsft4->texture = texture_from_list(tl, get_i16(bytes, &p));
prm.lsft4->cba = get_i16(bytes, &p);
prm.lsft4->tsb = get_i16(bytes, &p);
prm.lsft4->u0 = get_i8(bytes, &p);
prm.lsft4->v0 = get_i8(bytes, &p);
prm.lsft4->u1 = get_i8(bytes, &p);
prm.lsft4->v1 = get_i8(bytes, &p);
prm.lsft4->u2 = get_i8(bytes, &p);
prm.lsft4->v2 = get_i8(bytes, &p);
prm.lsft4->u3 = get_i8(bytes, &p);
prm.lsft4->v3 = get_i8(bytes, &p);
prm.lsft4->colour = int32_to_rgba(get_i32(bytes, &p));
break;
case PRM_TYPE_LSG3:
prm.ptr = mem_bump(sizeof(LSG3));
prm.lsg3->coords[0] = get_i16(bytes, &p);
prm.lsg3->coords[1] = get_i16(bytes, &p);
prm.lsg3->coords[2] = get_i16(bytes, &p);
prm.lsg3->normals[0] = get_i16(bytes, &p);
prm.lsg3->normals[1] = get_i16(bytes, &p);
prm.lsg3->normals[2] = get_i16(bytes, &p);
prm.lsg3->colour[0] = int32_to_rgba(get_i32(bytes, &p));
prm.lsg3->colour[1] = int32_to_rgba(get_i32(bytes, &p));
prm.lsg3->colour[2] = int32_to_rgba(get_i32(bytes, &p));
break;
case PRM_TYPE_LSG4:
prm.ptr = mem_bump(sizeof(LSG4));
prm.lsg4->coords[0] = get_i16(bytes, &p);
prm.lsg4->coords[1] = get_i16(bytes, &p);
prm.lsg4->coords[2] = get_i16(bytes, &p);
prm.lsg4->coords[3] = get_i16(bytes, &p);
prm.lsg4->normals[0] = get_i16(bytes, &p);
prm.lsg4->normals[1] = get_i16(bytes, &p);
prm.lsg4->normals[2] = get_i16(bytes, &p);
prm.lsg4->normals[3] = get_i16(bytes, &p);
prm.lsg4->colour[0] = int32_to_rgba(get_i32(bytes, &p));
prm.lsg4->colour[1] = int32_to_rgba(get_i32(bytes, &p));
prm.lsg4->colour[2] = int32_to_rgba(get_i32(bytes, &p));
prm.lsg4->colour[3] = int32_to_rgba(get_i32(bytes, &p));
break;
case PRM_TYPE_LSGT3:
prm.ptr = mem_bump(sizeof(LSGT3));
prm.lsgt3->coords[0] = get_i16(bytes, &p);
prm.lsgt3->coords[1] = get_i16(bytes, &p);
prm.lsgt3->coords[2] = get_i16(bytes, &p);
prm.lsgt3->normals[0] = get_i16(bytes, &p);
prm.lsgt3->normals[1] = get_i16(bytes, &p);
prm.lsgt3->normals[2] = get_i16(bytes, &p);
prm.lsgt3->texture = texture_from_list(tl, get_i16(bytes, &p));
prm.lsgt3->cba = get_i16(bytes, &p);
prm.lsgt3->tsb = get_i16(bytes, &p);
prm.lsgt3->u0 = get_i8(bytes, &p);
prm.lsgt3->v0 = get_i8(bytes, &p);
prm.lsgt3->u1 = get_i8(bytes, &p);
prm.lsgt3->v1 = get_i8(bytes, &p);
prm.lsgt3->u2 = get_i8(bytes, &p);
prm.lsgt3->v2 = get_i8(bytes, &p);
prm.lsgt3->colour[0] = int32_to_rgba(get_i32(bytes, &p));
prm.lsgt3->colour[1] = int32_to_rgba(get_i32(bytes, &p));
prm.lsgt3->colour[2] = int32_to_rgba(get_i32(bytes, &p));
break;
case PRM_TYPE_LSGT4:
prm.ptr = mem_bump(sizeof(LSGT4));
prm.lsgt4->coords[0] = get_i16(bytes, &p);
prm.lsgt4->coords[1] = get_i16(bytes, &p);
prm.lsgt4->coords[2] = get_i16(bytes, &p);
prm.lsgt4->coords[3] = get_i16(bytes, &p);
prm.lsgt4->normals[0] = get_i16(bytes, &p);
prm.lsgt4->normals[1] = get_i16(bytes, &p);
prm.lsgt4->normals[2] = get_i16(bytes, &p);
prm.lsgt4->normals[3] = get_i16(bytes, &p);
prm.lsgt4->texture = texture_from_list(tl, get_i16(bytes, &p));
prm.lsgt4->cba = get_i16(bytes, &p);
prm.lsgt4->tsb = get_i16(bytes, &p);
prm.lsgt4->u0 = get_i8(bytes, &p);
prm.lsgt4->v0 = get_i8(bytes, &p);
prm.lsgt4->u1 = get_i8(bytes, &p);
prm.lsgt4->v1 = get_i8(bytes, &p);
prm.lsgt4->u2 = get_i8(bytes, &p);
prm.lsgt4->v2 = get_i8(bytes, &p);
prm.lsgt4->pad1 = get_i16(bytes, &p);
prm.lsgt4->colour[0] = int32_to_rgba(get_i32(bytes, &p));
prm.lsgt4->colour[1] = int32_to_rgba(get_i32(bytes, &p));
prm.lsgt4->colour[2] = int32_to_rgba(get_i32(bytes, &p));
prm.lsgt4->colour[3] = int32_to_rgba(get_i32(bytes, &p));
break;
case PRM_TYPE_TSPR:
case PRM_TYPE_BSPR:
prm.ptr = mem_bump(sizeof(SPR));
prm.spr->coord = get_i16(bytes, &p);
prm.spr->width = get_i16(bytes, &p);
prm.spr->height = get_i16(bytes, &p);
prm.spr->texture = texture_from_list(tl, get_i16(bytes, &p));
prm.spr->colour = int32_to_rgba(get_i32(bytes, &p));
break;
case PRM_TYPE_SPLINE:
prm.ptr = mem_bump(sizeof(Spline));
prm.spline->control1.x = get_i32(bytes, &p);
prm.spline->control1.y = get_i32(bytes, &p);
prm.spline->control1.z = get_i32(bytes, &p);
p += 4; // padding
prm.spline->position.x = get_i32(bytes, &p);
prm.spline->position.y = get_i32(bytes, &p);
prm.spline->position.z = get_i32(bytes, &p);
p += 4; // padding
prm.spline->control2.x = get_i32(bytes, &p);
prm.spline->control2.y = get_i32(bytes, &p);
prm.spline->control2.z = get_i32(bytes, &p);
p += 4; // padding
prm.spline->colour = int32_to_rgba(get_i32(bytes, &p));
break;
case PRM_TYPE_POINT_LIGHT:
prm.ptr = mem_bump(sizeof(PointLight));
prm.pointLight->position.x = get_i32(bytes, &p);
prm.pointLight->position.y = get_i32(bytes, &p);
prm.pointLight->position.z = get_i32(bytes, &p);
p += 4; // padding
prm.pointLight->colour = int32_to_rgba(get_i32(bytes, &p));
prm.pointLight->startFalloff = get_i16(bytes, &p);
prm.pointLight->endFalloff = get_i16(bytes, &p);
break;
case PRM_TYPE_SPOT_LIGHT:
prm.ptr = mem_bump(sizeof(SpotLight));
prm.spotLight->position.x = get_i32(bytes, &p);
prm.spotLight->position.y = get_i32(bytes, &p);
prm.spotLight->position.z = get_i32(bytes, &p);
p += 4; // padding
prm.spotLight->direction.x = get_i16(bytes, &p);
prm.spotLight->direction.y = get_i16(bytes, &p);
prm.spotLight->direction.z = get_i16(bytes, &p);
p += 2; // padding
prm.spotLight->colour = int32_to_rgba(get_i32(bytes, &p));
prm.spotLight->startFalloff = get_i16(bytes, &p);
prm.spotLight->endFalloff = get_i16(bytes, &p);
prm.spotLight->coneAngle = get_i16(bytes, &p);
prm.spotLight->spreadAngle = get_i16(bytes, &p);
break;
case PRM_TYPE_INFINITE_LIGHT:
prm.ptr = mem_bump(sizeof(InfiniteLight));
prm.infiniteLight->direction.x = get_i16(bytes, &p);
prm.infiniteLight->direction.y = get_i16(bytes, &p);
prm.infiniteLight->direction.z = get_i16(bytes, &p);
p += 2; // padding
prm.infiniteLight->colour = int32_to_rgba(get_i32(bytes, &p));
break;
default:
die("bad primitive type %x \n", prm_type);
} // switch
prm.f3->type = prm_type;
prm.f3->flag = prm_flag;
} // each prim
} // each object
mem_temp_free(bytes);
return objectList;
}
void object_draw(Object *object, mat4_t *mat) {
vec3_t *vertex = object->vertices;
Prm poly = {.primitive = object->primitives};
int primitives_len = object->primitives_len;
render_set_model_mat(mat);
// TODO: check for PRM_SINGLE_SIDED
for (int i = 0; i < primitives_len; i++) {
int coord0;
int coord1;
int coord2;
int coord3;
switch (poly.primitive->type) {
case PRM_TYPE_GT3:
coord0 = poly.gt3->coords[0];
coord1 = poly.gt3->coords[1];
coord2 = poly.gt3->coords[2];
render_push_tris((tris_t) {
.vertices = {
{
.pos = vertex[coord2],
.uv = {poly.gt3->u2, poly.gt3->v2},
.color = poly.gt3->colour[2]
},
{
.pos = vertex[coord1],
.uv = {poly.gt3->u1, poly.gt3->v1},
.color = poly.gt3->colour[1]
},
{
.pos = vertex[coord0],
.uv = {poly.gt3->u0, poly.gt3->v0},
.color = poly.gt3->colour[0]
},
}
}, poly.gt3->texture);
poly.gt3 += 1;
break;
case PRM_TYPE_GT4:
coord0 = poly.gt4->coords[0];
coord1 = poly.gt4->coords[1];
coord2 = poly.gt4->coords[2];
coord3 = poly.gt4->coords[3];
render_push_tris((tris_t) {
.vertices = {
{
.pos = vertex[coord2],
.uv = {poly.gt4->u2, poly.gt4->v2},
.color = poly.gt4->colour[2]
},
{
.pos = vertex[coord1],
.uv = {poly.gt4->u1, poly.gt4->v1},
.color = poly.gt4->colour[1]
},
{
.pos = vertex[coord0],
.uv = {poly.gt4->u0, poly.gt4->v0},
.color = poly.gt4->colour[0]
},
}
}, poly.gt4->texture);
render_push_tris((tris_t) {
.vertices = {
{
.pos = vertex[coord2],
.uv = {poly.gt4->u2, poly.gt4->v2},
.color = poly.gt4->colour[2]
},
{
.pos = vertex[coord3],
.uv = {poly.gt4->u3, poly.gt4->v3},
.color = poly.gt4->colour[3]
},
{
.pos = vertex[coord1],
.uv = {poly.gt4->u1, poly.gt4->v1},
.color = poly.gt4->colour[1]
},
}
}, poly.gt4->texture);
poly.gt4 += 1;
break;
case PRM_TYPE_FT3:
coord0 = poly.ft3->coords[0];
coord1 = poly.ft3->coords[1];
coord2 = poly.ft3->coords[2];
render_push_tris((tris_t) {
.vertices = {
{
.pos = vertex[coord2],
.uv = {poly.ft3->u2, poly.ft3->v2},
.color = poly.ft3->colour
},
{
.pos = vertex[coord1],
.uv = {poly.ft3->u1, poly.ft3->v1},
.color = poly.ft3->colour
},
{
.pos = vertex[coord0],
.uv = {poly.ft3->u0, poly.ft3->v0},
.color = poly.ft3->colour
},
}
}, poly.ft3->texture);
poly.ft3 += 1;
break;
case PRM_TYPE_FT4:
coord0 = poly.ft4->coords[0];
coord1 = poly.ft4->coords[1];
coord2 = poly.ft4->coords[2];
coord3 = poly.ft4->coords[3];
render_push_tris((tris_t) {
.vertices = {
{
.pos = vertex[coord2],
.uv = {poly.ft4->u2, poly.ft4->v2},
.color = poly.ft4->colour
},
{
.pos = vertex[coord1],
.uv = {poly.ft4->u1, poly.ft4->v1},
.color = poly.ft4->colour
},
{
.pos = vertex[coord0],
.uv = {poly.ft4->u0, poly.ft4->v0},
.color = poly.ft4->colour
},
}
}, poly.ft4->texture);
render_push_tris((tris_t) {
.vertices = {
{
.pos = vertex[coord2],
.uv = {poly.ft4->u2, poly.ft4->v2},
.color = poly.ft4->colour
},
{
.pos = vertex[coord3],
.uv = {poly.ft4->u3, poly.ft4->v3},
.color = poly.ft4->colour
},
{
.pos = vertex[coord1],
.uv = {poly.ft4->u1, poly.ft4->v1},
.color = poly.ft4->colour
},
}
}, poly.ft4->texture);
poly.ft4 += 1;
break;
case PRM_TYPE_G3:
coord0 = poly.g3->coords[0];
coord1 = poly.g3->coords[1];
coord2 = poly.g3->coords[2];
render_push_tris((tris_t) {
.vertices = {
{
.pos = vertex[coord2],
.color = poly.g3->colour[2]
},
{
.pos = vertex[coord1],
.color = poly.g3->colour[1]
},
{
.pos = vertex[coord0],
.color = poly.g3->colour[0]
},
}
}, RENDER_NO_TEXTURE);
poly.g3 += 1;
break;
case PRM_TYPE_G4:
coord0 = poly.g4->coords[0];
coord1 = poly.g4->coords[1];
coord2 = poly.g4->coords[2];
coord3 = poly.g4->coords[3];
render_push_tris((tris_t) {
.vertices = {
{
.pos = vertex[coord2],
.color = poly.g4->colour[2]
},
{
.pos = vertex[coord1],
.color = poly.g4->colour[1]
},
{
.pos = vertex[coord0],
.color = poly.g4->colour[0]
},
}
}, RENDER_NO_TEXTURE);
render_push_tris((tris_t) {
.vertices = {
{
.pos = vertex[coord2],
.color = poly.g4->colour[2]
},
{
.pos = vertex[coord3],
.color = poly.g4->colour[3]
},
{
.pos = vertex[coord1],
.color = poly.g4->colour[1]
},
}
}, RENDER_NO_TEXTURE);
poly.g4 += 1;
break;
case PRM_TYPE_F3:
coord0 = poly.f3->coords[0];
coord1 = poly.f3->coords[1];
coord2 = poly.f3->coords[2];
render_push_tris((tris_t) {
.vertices = {
{
.pos = vertex[coord2],
.color = poly.f3->colour
},
{
.pos = vertex[coord1],
.color = poly.f3->colour
},
{
.pos = vertex[coord0],
.color = poly.f3->colour
},
}
}, RENDER_NO_TEXTURE);
poly.f3 += 1;
break;
case PRM_TYPE_F4:
coord0 = poly.f4->coords[0];
coord1 = poly.f4->coords[1];
coord2 = poly.f4->coords[2];
coord3 = poly.f4->coords[3];
render_push_tris((tris_t) {
.vertices = {
{
.pos = vertex[coord2],
.color = poly.f4->colour
},
{
.pos = vertex[coord1],
.color = poly.f4->colour
},
{
.pos = vertex[coord0],
.color = poly.f4->colour
},
}
}, RENDER_NO_TEXTURE);
render_push_tris((tris_t) {
.vertices = {
{
.pos = vertex[coord2],
.color = poly.f4->colour
},
{
.pos = vertex[coord3],
.color = poly.f4->colour
},
{
.pos = vertex[coord1],
.color = poly.f4->colour
},
}
}, RENDER_NO_TEXTURE);
poly.f4 += 1;
break;
case PRM_TYPE_TSPR:
case PRM_TYPE_BSPR:
coord0 = poly.spr->coord;
render_push_sprite(
vec3(
vertex[coord0].x,
vertex[coord0].y + ((poly.primitive->type == PRM_TYPE_TSPR ? poly.spr->height : -poly.spr->height) >> 1),
vertex[coord0].z
),
vec2i(poly.spr->width, poly.spr->height),
poly.spr->colour,
poly.spr->texture
);
poly.spr += 1;
break;
default:
break;
}
}
}

@ -0,0 +1,383 @@
#ifndef OBJECT_H
#define OBJECT_H
#include "../types.h"
#include "../render.h"
#include "../utils.h"
#include "image.h"
// Primitive Structure Stub ( Structure varies with primitive type )
typedef struct Primitive {
int16_t type; // Type of Primitive
} Primitive;
typedef struct F3 {
int16_t type; // Type of primitive
int16_t flag;
int16_t coords[3]; // Indices of the coords
int16_t pad1;
rgba_t colour;
} F3;
typedef struct FT3 {
int16_t type; // Type of primitive
int16_t flag;
int16_t coords[3]; // Indices of the coords
int16_t texture;
int16_t cba;
int16_t tsb;
uint8_t u0;
uint8_t v0;
uint8_t u1;
uint8_t v1;
uint8_t u2;
uint8_t v2;
int16_t pad1;
rgba_t colour;
} FT3;
typedef struct F4 {
int16_t type; // Type of primitive
int16_t flag;
int16_t coords[4]; // Indices of the coords
rgba_t colour;
} F4;
typedef struct FT4 {
int16_t type; // Type of primitive
int16_t flag;
int16_t coords[4]; // Indices of the coords
int16_t texture;
int16_t cba;
int16_t tsb;
uint8_t u0;
uint8_t v0;
uint8_t u1;
uint8_t v1;
uint8_t u2;
uint8_t v2;
uint8_t u3;
uint8_t v3;
int16_t pad1;
rgba_t colour;
} FT4;
typedef struct G3 {
int16_t type; // Type of primitive
int16_t flag;
int16_t coords[3]; // Indices of the coords
int16_t pad1;
rgba_t colour[3];
} G3;
typedef struct GT3 {
int16_t type; // Type of primitive
int16_t flag;
int16_t coords[3]; // Indices of the coords
int16_t texture;
int16_t cba;
int16_t tsb;
uint8_t u0;
uint8_t v0;
uint8_t u1;
uint8_t v1;
uint8_t u2;
uint8_t v2;
int16_t pad1;
rgba_t colour[3];
} GT3;
typedef struct G4 {
int16_t type; // Type of primitive
int16_t flag;
int16_t coords[4]; // Indices of the coords
rgba_t colour[4];
} G4;
typedef struct GT4 {
int16_t type; // Type of primitive
int16_t flag;
int16_t coords[4]; // Indices of the coords
int16_t texture;
int16_t cba;
int16_t tsb;
uint8_t u0;
uint8_t v0;
uint8_t u1;
uint8_t v1;
uint8_t u2;
uint8_t v2;
uint8_t u3;
uint8_t v3;
int16_t pad1;
rgba_t colour[4];
} GT4;
/* LIGHT SOURCED POLYGONS
*/
typedef struct LSF3 {
int16_t type; // Type of primitive
int16_t flag;
int16_t coords[3]; // Indices of the coords
int16_t normal; // Indices of the normals
rgba_t colour;
} LSF3;
typedef struct LSFT3 {
int16_t type; // Type of primitive
int16_t flag;
int16_t coords[3]; // Indices of the coords
int16_t normal; // Indices of the normals
int16_t texture;
int16_t cba;
int16_t tsb;
uint8_t u0;
uint8_t v0;
uint8_t u1;
uint8_t v1;
uint8_t u2;
uint8_t v2;
rgba_t colour;
} LSFT3;
typedef struct LSF4 {
int16_t type; // Type of primitive
int16_t flag;
int16_t coords[4]; // Indices of the coords
int16_t normal; // Indices of the normals
int16_t pad1;
rgba_t colour;
} LSF4;
typedef struct LSFT4 {
int16_t type; // Type of primitive
int16_t flag;
int16_t coords[4]; // Indices of the coords
int16_t normal; // Indices of the normals
int16_t texture;
int16_t cba;
int16_t tsb;
uint8_t u0;
uint8_t v0;
uint8_t u1;
uint8_t v1;
uint8_t u2;
uint8_t v2;
uint8_t u3;
uint8_t v3;
rgba_t colour;
} LSFT4;
typedef struct LSG3 {
int16_t type; // Type of primitive
int16_t flag;
int16_t coords[3]; // Indices of the coords
int16_t normals[3]; // Indices of the normals
rgba_t colour[3];
} LSG3;
typedef struct LSGT3 {
int16_t type; // Type of primitive
int16_t flag;
int16_t coords[3]; // Indices of the coords
int16_t normals[3]; // Indices of the normals
int16_t texture;
int16_t cba;
int16_t tsb;
uint8_t u0;
uint8_t v0;
uint8_t u1;
uint8_t v1;
uint8_t u2;
uint8_t v2;
rgba_t colour[3];
} LSGT3;
typedef struct LSG4 {
int16_t type; // Type of primitive
int16_t flag;
int16_t coords[4]; // Indices of the coords
int16_t normals[4]; // Indices of the normals
rgba_t colour[4];
} LSG4;
typedef struct LSGT4 {
int16_t type; // Type of primitive
int16_t flag;
int16_t coords[4]; // Indices of the coords
int16_t normals[4]; // Indices of the normals
int16_t texture;
int16_t cba;
int16_t tsb;
uint8_t u0;
uint8_t v0;
uint8_t u1;
uint8_t v1;
uint8_t u2;
uint8_t v2;
uint8_t u3;
uint8_t v3;
int16_t pad1;
rgba_t colour[4];
} LSGT4;
/* OTHER PRIMITIVE TYPES
*/
typedef struct SPR {
int16_t type;
int16_t flag;
int16_t coord;
int16_t width;
int16_t height;
int16_t texture;
rgba_t colour;
} SPR;
typedef struct Spline {
int16_t type; // Type of primitive
int16_t flag;
vec3_t control1;
vec3_t position;
vec3_t control2;
rgba_t colour;
} Spline;
typedef struct PointLight {
int16_t type;
int16_t flag;
vec3_t position;
rgba_t colour;
int16_t startFalloff;
int16_t endFalloff;
} PointLight;
typedef struct SpotLight {
int16_t type;
int16_t flag;
vec3_t position;
vec3_t direction;
rgba_t colour;
int16_t startFalloff;
int16_t endFalloff;
int16_t coneAngle;
int16_t spreadAngle;
} SpotLight;
typedef struct InfiniteLight {
int16_t type;
int16_t flag;
vec3_t direction;
rgba_t colour;
} InfiniteLight;
// PRIMITIVE FLAGS
#define PRM_SINGLE_SIDED 0x0001
#define PRM_SHIP_ENGINE 0x0002
#define PRM_TRANSLUCENT 0x0004
#define PRM_TYPE_F3 1
#define PRM_TYPE_FT3 2
#define PRM_TYPE_F4 3
#define PRM_TYPE_FT4 4
#define PRM_TYPE_G3 5
#define PRM_TYPE_GT3 6
#define PRM_TYPE_G4 7
#define PRM_TYPE_GT4 8
#define PRM_TYPE_LF2 9
#define PRM_TYPE_TSPR 10
#define PRM_TYPE_BSPR 11
#define PRM_TYPE_LSF3 12
#define PRM_TYPE_LSFT3 13
#define PRM_TYPE_LSF4 14
#define PRM_TYPE_LSFT4 15
#define PRM_TYPE_LSG3 16
#define PRM_TYPE_LSGT3 17
#define PRM_TYPE_LSG4 18
#define PRM_TYPE_LSGT4 19
#define PRM_TYPE_SPLINE 20
#define PRM_TYPE_INFINITE_LIGHT 21
#define PRM_TYPE_POINT_LIGHT 22
#define PRM_TYPE_SPOT_LIGHT 23
typedef struct Object {
char name[16];
mat4_t mat;
int16_t vertices_len; // Number of Vertices
vec3_t *vertices; // Pointer to 3D Points
int16_t normals_len; // Number of Normals
vec3_t *normals; // Pointer to 3D Normals
int16_t primitives_len; // Number of Primitives
Primitive *primitives; // Pointer to Z Sort Primitives
vec3_t origin;
int32_t extent; // Flags for object characteristics
int16_t flags; // Next object in list
struct Object *next; // Next object in list
} Object;
typedef union Prm {
uint8_t *ptr;
int16_t *sptr;
int32_t *lptr;
Object *object;
Primitive *primitive;
F3 *f3;
FT3 *ft3;
F4 *f4;
FT4 *ft4;
G3 *g3;
GT3 *gt3;
G4 *g4;
GT4 *gt4;
SPR *spr;
Spline *spline;
PointLight *pointLight;
SpotLight *spotLight;
InfiniteLight *infiniteLight;
LSF3 *lsf3;
LSFT3 *lsft3;
LSF4 *lsf4;
LSFT4 *lsft4;
LSG3 *lsg3;
LSGT3 *lsgt3;
LSG4 *lsg4;
LSGT4 *lsgt4;
} Prm;
Object *objects_load(char *name, texture_list_t tl);
void object_draw(Object *object, mat4_t *mat);
#endif

@ -0,0 +1,70 @@
#include "../mem.h"
#include "../utils.h"
#include "../system.h"
#include "../render.h"
#include "particle.h"
#include "image.h"
static particle_t *particles;
static int particles_active = 0;
static texture_list_t particle_textures;
void particles_load() {
particles = mem_bump(sizeof(particle_t) * PARTICLES_MAX);
particle_textures = image_get_compressed_textures("wipeout/common/effects.cmp");
particles_init();
}
void particles_init() {
particles_active = 0;
}
void particles_update() {
for (int i = 0; i < particles_active; i++) {
particle_t *p = &particles[i];
p->timer -= system_tick();
p->position = vec3_add(p->position, vec3_mulf(p->velocity, system_tick()));
if (p->timer < 0) {
particles[i--] = particles[--particles_active];
continue;
}
}
}
void particles_draw() {
if (particles_active == 0) {
return;
}
render_set_model_mat(&mat4_identity());
render_set_depth_write(false);
render_set_blend_mode(RENDER_BLEND_LIGHTER);
render_set_depth_offset(-32.0);
for (int i = 0; i < particles_active; i++) {
particle_t *p = &particles[i];
render_push_sprite(p->position, p->size, p->color, p->texture);
}
render_set_depth_offset(0.0);
render_set_depth_write(true);
render_set_blend_mode(RENDER_BLEND_NORMAL);
}
void particles_spawn(vec3_t position, uint16_t type, vec3_t velocity, int size) {
if (particles_active == PARTICLES_MAX) {
return;
}
particle_t *p = &particles[particles_active++];
p->color = rgba(128, 128, 128, 128);
p->texture = texture_from_list(particle_textures, type);
p->position = position;
p->velocity = velocity;
p->timer = rand_float(0.75, 1.0);
p->size.x = size;
p->size.y = size;
}

@ -0,0 +1,32 @@
#ifndef PARTICLE_H
#define PARTICLE_H
#include "../types.h"
#define PARTICLES_MAX 1024
#define PARTICLE_TYPE_NONE -1
#define PARTICLE_TYPE_FIRE 0
#define PARTICLE_TYPE_FIRE_WHITE 1
#define PARTICLE_TYPE_SMOKE 2
#define PARTICLE_TYPE_EBOLT 3
#define PARTICLE_TYPE_HALO 4
#define PARTICLE_TYPE_GREENY 5
typedef struct particle_t {
vec3_t position;
vec3_t velocity;
vec2i_t size;
rgba_t color;
float timer;
uint16_t type;
uint16_t texture;
} particle_t;
void particles_load();
void particles_init();
void particles_spawn(vec3_t position, uint16_t type, vec3_t velocity, int size);
void particles_draw();
void particles_update();
#endif

@ -0,0 +1,279 @@
#include "../mem.h"
#include "../input.h"
#include "../platform.h"
#include "../system.h"
#include "../utils.h"
#include "object.h"
#include "track.h"
#include "ship.h"
#include "weapon.h"
#include "droid.h"
#include "camera.h"
#include "object.h"
#include "scene.h"
#include "game.h"
#include "hud.h"
#include "sfx.h"
#include "race.h"
#include "particle.h"
#include "menu.h"
#include "ship_ai.h"
#include "ingame_menus.h"
#define ATTRACT_DURATION 60.0
static bool is_paused = false;
static bool menu_is_scroll_text = false;
static bool has_show_credits = false;
static float attract_start_time;
static menu_t *active_menu = NULL;
void race_init() {
ingame_menus_load();
menu_is_scroll_text = false;
const circut_settings_t *cs = &def.circuts[g.circut].settings[g.race_class];
track_load(cs->path);
scene_load(cs->path, cs->sky_y_offset);
if (g.circut == CIRCUT_SILVERSTREAM && g.race_class == RACE_CLASS_RAPIER) {
scene_init_aurora_borealis();
}
race_start();
// render_textures_dump("texture_atlas.png");
if (g.is_attract_mode) {
attract_start_time = system_time();
for (int i = 0; i < len(g.ships); i++) {
// FIXME: this is needed to initializes the engine sound. Should
// maybe be done in a separate step?
ship_ai_update_intro(&g.ships[i]);
g.ships[i].update_func = ship_ai_update_race;
flags_rm(g.ships[i].flags, SHIP_VIEW_INTERNAL);
flags_rm(g.ships[i].flags, SHIP_RACING);
}
g.pilot = rand_int(0, len(def.pilots));
g.camera.update_func = camera_update_attract_random;
if (!has_show_credits || rand_int(0, 10) == 0) {
active_menu = text_scroll_menu_init(def.credits, len(def.credits));
menu_is_scroll_text = true;
has_show_credits = true;
}
}
is_paused = false;
}
void race_update() {
if (is_paused) {
if (!active_menu) {
active_menu = pause_menu_init();
}
if (input_pressed(A_MENU_QUIT)) {
race_unpause();
}
}
else {
ships_update();
droid_update(&g.droid, &g.ships[g.pilot]);
camera_update(&g.camera, &g.ships[g.pilot], &g.droid);
weapons_update();
particles_update();
scene_update();
if (g.race_type != RACE_TYPE_TIME_TRIAL) {
track_cycle_pickups();
}
if (g.is_attract_mode) {
if (input_pressed(A_MENU_START) || input_pressed(A_MENU_SELECT)) {
game_set_scene(GAME_SCENE_MAIN_MENU);
}
float duration = system_time() - attract_start_time;
if ((!active_menu && duration > 30) || duration > 120) {
game_set_scene(GAME_SCENE_TITLE);
}
}
else if (active_menu == NULL && (input_pressed(A_MENU_START) || input_pressed(A_MENU_QUIT))) {
race_pause();
}
}
// Draw 3D
render_set_view(g.camera.position, g.camera.angle);
render_set_cull_backface(false);
scene_draw(&g.camera);
track_draw(&g.camera);
render_set_cull_backface(true);
ships_draw();
droid_draw(&g.droid);
weapons_draw();
particles_draw();
// Draw 2d
render_set_view_2d();
if (flags_is(g.ships[g.pilot].flags, SHIP_RACING)) {
hud_draw(&g.ships[g.pilot]);
}
if (active_menu) {
if (!menu_is_scroll_text) {
vec2i_t size = render_size();
render_push_2d(vec2i(0, 0), size, rgba(0, 0, 0, 128), RENDER_NO_TEXTURE);
}
menu_update(active_menu);
}
}
void race_start() {
active_menu = NULL;
sfx_reset();
scene_init();
camera_init(&g.camera, g.track.sections);
g.camera.update_func = camera_update_race_intro;
ships_init(g.track.sections);
droid_init(&g.droid, &g.ships[g.pilot]);
particles_init();
weapons_init();
for (int i = 0; i < len(g.race_ranks); i++) {
g.race_ranks[i].points = 0;
g.race_ranks[i].pilot = i;
}
for (int i = 0; i < len(g.lap_times); i++) {
for (int j = 0; j < len(g.lap_times[i]); j++) {
g.lap_times[i][j] = 0;
}
}
g.is_new_race_record = false;
g.is_new_lap_record = false;
g.best_lap = 0;
g.race_time = 0;
}
void race_restart() {
race_unpause();
if (g.race_type == RACE_TYPE_CHAMPIONSHIP) {
g.lives--;
if (g.lives == 0) {
race_release_control();
active_menu = game_over_menu_init();
return;
}
}
race_start();
}
static bool sort_points_compare(pilot_points_t *pa, pilot_points_t *pb) {
return (pa->points < pb->points);
}
void race_end() {
race_release_control();
g.race_position = g.ships[g.pilot].position_rank;
g.race_time = 0;
g.best_lap = g.lap_times[g.pilot][0];
for (int i = 0; i < NUM_LAPS; i++) {
g.race_time += g.lap_times[g.pilot][i];
if (g.lap_times[g.pilot][i] < g.best_lap) {
g.best_lap = g.lap_times[g.pilot][i];
}
}
highscores_t *hs = &save.highscores[g.race_class][g.circut][g.highscore_tab];
if (g.best_lap < hs->lap_record) {
hs->lap_record = g.best_lap;
g.is_new_lap_record = true;
save.is_dirty = true;
}
for (int i = 0; i < NUM_HIGHSCORES; i++) {
if (g.race_time < hs->entries[i].time) {
g.is_new_race_record = true;
break;
}
}
if (g.race_type == RACE_TYPE_CHAMPIONSHIP) {
for (int i = 0; i < len(def.race_points_for_rank); i++) {
g.race_ranks[i].points = def.race_points_for_rank[i];
// Find the pilot for this race rank in the championship table
for (int j = 0; j < len(g.championship_ranks); j++) {
if (g.race_ranks[i].pilot == g.championship_ranks[j].pilot) {
g.championship_ranks[j].points += def.race_points_for_rank[i];
break;
}
}
}
sort(g.championship_ranks, len(g.championship_ranks), sort_points_compare);
}
active_menu = race_stats_menu_init();
}
void race_next() {
int next_circut = g.circut + 1;
// Championship complete
if (
(save.has_bonus_circuts && next_circut >= NUM_CIRCUTS) ||
(!save.has_bonus_circuts && next_circut >= NUM_NON_BONUS_CIRCUTS)
) {
if (g.race_class == RACE_CLASS_RAPIER) {
if (save.has_bonus_circuts) {
active_menu = text_scroll_menu_init(def.congratulations.rapier_all_circuts, len(def.congratulations.rapier_all_circuts));
}
else {
save.has_bonus_circuts = true;
active_menu = text_scroll_menu_init(def.congratulations.rapier, len(def.congratulations.rapier));
}
}
else {
save.has_rapier_class = true;
if (save.has_bonus_circuts) {
active_menu = text_scroll_menu_init(def.congratulations.venom_all_circuts, len(def.congratulations.venom_all_circuts));
}
else {
active_menu = text_scroll_menu_init(def.congratulations.venom, len(def.congratulations.venom));
}
}
save.is_dirty = true;
menu_is_scroll_text = true;
}
// Next track
else {
g.circut = next_circut;
game_set_scene(GAME_SCENE_RACE);
}
}
void race_release_control() {
flags_rm(g.ships[g.pilot].flags, SHIP_RACING);
g.ships[g.pilot].remote_thrust_max = 3160;
g.ships[g.pilot].remote_thrust_mag = 32;
g.ships[g.pilot].speed = 3160;
g.camera.update_func = camera_update_attract_random;
}
void race_pause() {
sfx_pause();
is_paused = true;
}
void race_unpause() {
sfx_unpause();
is_paused = false;
active_menu = NULL;
}

@ -0,0 +1,14 @@
#ifndef RACE_H
#define RACE_H
void race_init();
void race_update();
void race_start();
void race_restart();
void race_pause();
void race_unpause();
void race_end();
void race_next();
void race_release_control();
#endif

@ -0,0 +1,261 @@
#include "../mem.h"
#include "../utils.h"
#include "../system.h"
#include "object.h"
#include "track.h"
#include "ship.h"
#include "weapon.h"
#include "scene.h"
#include "droid.h"
#include "camera.h"
#include "object.h"
#include "game.h"
#define SCENE_START_BOOMS_MAX 4
#define SCENE_OIL_PUMPS_MAX 2
#define SCENE_RED_LIGHTS_MAX 4
#define SCENE_STANDS_MAX 20
static Object *scene_objects;
static Object *sky_object;
static vec3_t sky_offset;
static Object *start_booms[SCENE_START_BOOMS_MAX];
static int start_booms_len;
static Object *oil_pumps[SCENE_OIL_PUMPS_MAX];
static int oil_pumps_len;
static Object *red_lights[SCENE_RED_LIGHTS_MAX];
static int red_lights_len;
typedef struct {
sfx_t *sfx;
vec3_t pos;
} scene_stand_t;
static scene_stand_t stands[SCENE_STANDS_MAX];
static int stands_len;
static struct {
bool enabled;
GT4 *primitives[80];
int16_t *coords[80];
int16_t grey_coords[80];
} aurora_borealis;
void scene_pulsate_red_light(Object *obj);
void scene_move_oil_pump(Object *obj);
void scene_update_aurora_borealis();
void scene_load(const char *base_path, float sky_y_offset) {
texture_list_t scene_textures = image_get_compressed_textures(get_path(base_path, "scene.cmp"));
scene_objects = objects_load(get_path(base_path, "scene.prm"), scene_textures);
texture_list_t sky_textures = image_get_compressed_textures(get_path(base_path, "sky.cmp"));
sky_object = objects_load(get_path(base_path, "sky.prm") , sky_textures);
sky_offset = vec3(0, sky_y_offset, 0);
// Collect all objects that need to be updated each frame
start_booms_len = 0;
oil_pumps_len = 0;
red_lights_len = 0;
stands_len = 0;
Object *obj = scene_objects;
while (obj) {
mat4_set_translation(&obj->mat, obj->origin);
if (str_starts_with(obj->name, "start")) {
error_if(start_booms_len >= SCENE_START_BOOMS_MAX, "SCENE_START_BOOMS_MAX reached");
start_booms[start_booms_len++] = obj;
}
else if (str_starts_with(obj->name, "redl")) {
error_if(red_lights_len >= SCENE_RED_LIGHTS_MAX, "SCENE_RED_LIGHTS_MAX reached");
red_lights[red_lights_len++] = obj;
}
else if (str_starts_with(obj->name, "donkey")) {
error_if(oil_pumps_len >= SCENE_OIL_PUMPS_MAX, "SCENE_OIL_PUMPS_MAX reached");
oil_pumps[oil_pumps_len++] = obj;
}
else if (
str_starts_with(obj->name, "lostad") ||
str_starts_with(obj->name, "stad_") ||
str_starts_with(obj->name, "newstad_")
) {
error_if(stands_len >= SCENE_STANDS_MAX, "SCENE_STANDS_MAX reached");
stands[stands_len++] = (scene_stand_t){.sfx = NULL, .pos = obj->origin};
}
obj = obj->next;
}
aurora_borealis.enabled = false;
}
void scene_init() {
scene_set_start_booms(0);
for (int i = 0; i < stands_len; i++) {
stands[i].sfx = sfx_reserve_loop(SFX_CROWD);
}
}
void scene_update() {
for (int i = 0; i < red_lights_len; i++) {
scene_pulsate_red_light(red_lights[i]);
}
for (int i = 0; i < oil_pumps_len; i++) {
scene_move_oil_pump(oil_pumps[i]);
}
for (int i = 0; i < stands_len; i++) {
sfx_set_position(stands[i].sfx, stands[i].pos, vec3(0, 0, 0), 0.4);
}
if (aurora_borealis.enabled) {
scene_update_aurora_borealis();
}
}
void scene_draw(camera_t *camera) {
// Sky
render_set_depth_write(false);
mat4_set_translation(&sky_object->mat, vec3_add(camera->position, sky_offset));
object_draw(sky_object, &sky_object->mat);
render_set_depth_write(true);
// Nearby objects
vec3_t cam_pos = camera->position;
Object *object = scene_objects;
float max_dist_sq = RENDER_FADEOUT_FAR * RENDER_FADEOUT_FAR;
while (object) {
vec3_t d = vec3_sub(cam_pos, object->origin);
float dist_sq = d.x * d.x + d.y * d.y + d.z * d.z;
if (dist_sq < max_dist_sq) {
object_draw(object, &object->mat);
}
object = object->next;
}
}
void scene_set_start_booms(int light_index) {
int lights_len = 1;
rgba_t color = rgba(0, 0, 0, 0);
if (light_index == 0) { // reset all 3
lights_len = 3;
color = rgba(0x20, 0x20, 0x20, 0xff);
}
else if (light_index == 1) {
color = rgba(0xff, 0x00, 0x00, 0xff);
}
else if (light_index == 2) {
color = rgba(0xff, 0x80, 0x00, 0xff);
}
else if (light_index == 3) {
color = rgba(0x00, 0xff, 0x00, 0xff);
}
for (int i = 0; i < start_booms_len; i++) {
Prm libPoly = {.primitive = start_booms[i]->primitives};
for (int j = 1; j < light_index; j++) {
libPoly.gt4 += 1;
}
for (int j = 0; j < lights_len; j++) {
for (int v = 0; v < 4; v++) {
libPoly.gt4->colour[v].as_rgba.r = color.as_rgba.r;
libPoly.gt4->colour[v].as_rgba.g = color.as_rgba.g;
libPoly.gt4->colour[v].as_rgba.b = color.as_rgba.b;
}
libPoly.gt4 += 1;
}
}
}
void scene_pulsate_red_light(Object *obj) {
uint8_t r = clamp(sin(system_cycle_time() * M_PI * 2) * 128 + 128, 0, 255);
Prm libPoly = {.primitive = obj->primitives};
for (int v = 0; v < 4; v++) {
libPoly.gt4->colour[v].as_rgba.r = r;
libPoly.gt4->colour[v].as_rgba.g = 0x00;
libPoly.gt4->colour[v].as_rgba.b = 0x00;
}
}
void scene_move_oil_pump(Object *pump) {
mat4_set_yaw_pitch_roll(&pump->mat, vec3(sin(system_cycle_time() * 0.125 * M_PI * 2), 0, 0));
}
void scene_init_aurora_borealis() {
aurora_borealis.enabled = true;
clear(aurora_borealis.grey_coords);
int count = 0;
int16_t *coords;
float y;
Prm poly = {.primitive = sky_object->primitives};
for (int i = 0; i < sky_object->primitives_len; i++) {
switch (poly.primitive->type) {
case PRM_TYPE_GT3:
poly.gt3 += 1;
break;
case PRM_TYPE_GT4:
coords = poly.gt4->coords;
y = sky_object->vertices[coords[0]].y;
if (y < -6000) { // -8000
aurora_borealis.primitives[count] = poly.gt4;
if (y > -6800) {
aurora_borealis.coords[count] = poly.gt4->coords;
aurora_borealis.grey_coords[count] = -1;
}
else if (y < -11000) {
aurora_borealis.coords[count] = poly.gt4->coords;
aurora_borealis.grey_coords[count] = -2;
}
else {
aurora_borealis.coords[count] = poly.gt4->coords;
}
count++;
}
poly.gt4 += 1;
break;
}
}
}
void scene_update_aurora_borealis() {
float phase = system_time() / 30.0;
for (int i = 0; i < 80; i++) {
int16_t *coords = aurora_borealis.coords[i];
if (aurora_borealis.grey_coords[i] != -2) {
aurora_borealis.primitives[i]->colour[0].as_rgba.r = (sin(coords[0] * phase) * 64.0) + 190;
aurora_borealis.primitives[i]->colour[0].as_rgba.g = (sin(coords[0] * (phase + 0.054)) * 64.0) + 190;
aurora_borealis.primitives[i]->colour[0].as_rgba.b = (sin(coords[0] * (phase + 0.039)) * 64.0) + 190;
}
if (aurora_borealis.grey_coords[i] != -2) {
aurora_borealis.primitives[i]->colour[1].as_rgba.r = (sin(coords[1] * phase) * 64.0) + 190;
aurora_borealis.primitives[i]->colour[1].as_rgba.g = (sin(coords[1] * (phase + 0.054)) * 64.0) + 190;
aurora_borealis.primitives[i]->colour[1].as_rgba.b = (sin(coords[1] * (phase + 0.039)) * 64.0) + 190;
}
if (aurora_borealis.grey_coords[i] != -1) {
aurora_borealis.primitives[i]->colour[2].as_rgba.r = (sin(coords[2] * phase) * 64.0) + 190;
aurora_borealis.primitives[i]->colour[2].as_rgba.g = (sin(coords[2] * (phase + 0.054)) * 64.0) + 190;
aurora_borealis.primitives[i]->colour[2].as_rgba.b = (sin(coords[2] * (phase + 0.039)) * 64.0) + 190;
}
if (aurora_borealis.grey_coords[i] != -1) {
aurora_borealis.primitives[i]->colour[3].as_rgba.r = (sin(coords[3] * phase) * 64.0) + 190;
aurora_borealis.primitives[i]->colour[3].as_rgba.g = (sin(coords[3] * (phase + 0.054)) * 64.0) + 190;
aurora_borealis.primitives[i]->colour[3].as_rgba.b = (sin(coords[3] * (phase + 0.039)) * 64.0) + 190;
}
}
}

@ -0,0 +1,15 @@
#ifndef SCENE_H
#define SCENE_H
#include "image.h"
#include "camera.h"
void scene_load(const char *path, float sky_y_offset);
void scene_draw(camera_t *camera);
void scene_init();
void scene_set_start_booms(int num_lights);
void scene_init_aurora_borealis();
void scene_update();
void scene_draw();
#endif

@ -0,0 +1,393 @@
#include "../utils.h"
#include "../mem.h"
#include "../platform.h"
#include "sfx.h"
#include "game.h"
#define QOA_IMPLEMENTATION
#define QOA_NO_STDIO
#include "../libs/qoa.h"
typedef struct {
int16_t *samples;
uint32_t len;
} sfx_data_t;
typedef struct {
qoa_desc qoa;
FILE *file;
uint32_t track_index;
uint32_t first_frame_pos;
uint32_t buffer_len;
uint8_t *buffer;
uint32_t sample_data_pos;
uint32_t sample_data_len;
short *sample_data;
sfx_music_mode_t mode;
} music_decoder_t;
enum {
VAG_REGION_START = 1,
VAG_REGION = 2,
VAG_REGION_END = 4
};
static const int32_t vag_tab[5][2] = {
{ 0, 0}, // { 0.0, 0.0}, << 14
{15360, 0}, // { 60.0 / 64.0, 0.0}, << 14
{29440, -13312}, // {115.0 / 64.0, -52.0 / 64.0}, << 14
{25088, -14080}, // { 98.0 / 64.0, -55.0 / 64.0}, << 14
{31232, -15360}, // {122.0 / 64.0, -60.0 / 64.0}, << 14
};
static sfx_data_t *sources;
static uint32_t num_sources;
static sfx_t *nodes;
static music_decoder_t *music;
static void (*external_mix_cb)(float *, uint32_t len) = NULL;
void sfx_load() {
// Init decode buffer for music
uint32_t channels = 2;
music = mem_bump(sizeof(music_decoder_t));
music->buffer = mem_bump(QOA_FRAME_SIZE(channels, QOA_SLICES_PER_FRAME));
music->sample_data = mem_bump(channels * QOA_FRAME_LEN * sizeof(short) * 2);
music->qoa.channels = channels;
music->mode = SFX_MUSIC_RANDOM;
music->file = NULL;
music->track_index = -1;
// Load SFX samples
nodes = mem_bump(SFX_MAX * sizeof(sfx_t));
// 16 byte blocks: 2 byte header, 14 bytes with 2x4bit samples each
uint32_t vb_size;
uint8_t *vb = file_load("wipeout/sound/wipeout.vb", &vb_size);
uint32_t num_samples = (vb_size / 16) * 28;
int16_t *sample_buffer = mem_bump(num_samples * sizeof(int16_t));
sources = mem_mark();
num_sources = 0;
uint32_t sample_index = 0;
int32_t history[2] = {0, 0};
for (int p = 0; p < vb_size;) {
uint8_t header = vb[p++];
uint8_t flags = vb[p++];
uint8_t shift = header & 0x0f;
uint8_t predictor = clamp(header >> 4, 0, 4);
if (flags_is(flags, VAG_REGION_END)) {
mem_bump(sizeof(sfx_data_t));
sources[num_sources].samples = &sample_buffer[sample_index];
}
for (uint32_t bs = 0; bs < 14; bs++) {
int32_t nibbles[2] = {
(vb[p] & 0x0f) << 12,
(vb[p] & 0xf0) << 8
};
p++;
for (int ni = 0; ni < 2; ni++) {
int32_t sample = nibbles[ni];
if (sample & 0x8000) {
sample |= 0xffff0000;
}
sample >>= shift;
sample += (history[0] * vag_tab[predictor][0] + history[1] * vag_tab[predictor][1]) >> 14;
history[1] = history[0];
history[0] = sample;
sample_buffer[sample_index++] = clamp(sample, -32768, 32767);
}
}
if (flags_is(flags, VAG_REGION_START)) {
error_if(sources[num_sources].samples == NULL, "VAG_REGION_START without VAG_REGION_END");
sources[num_sources].len = &sample_buffer[sample_index] - sources[num_sources].samples;
num_sources++;
}
}
mem_temp_free(vb);
platform_set_audio_mix_cb(sfx_stero_mix);
}
void sfx_reset() {
for (int i = 0; i < SFX_MAX; i++) {
if (flags_is(nodes[i].flags, SFX_LOOP)) {
flags_set(nodes[i].flags, SFX_NONE);
}
}
}
void sfx_unpause() {
for (int i = 0; i < SFX_MAX; i++) {
if (flags_is(nodes[i].flags, SFX_LOOP_PAUSE)) {
flags_rm(nodes[i].flags, SFX_LOOP_PAUSE);
flags_add(nodes[i].flags, SFX_PLAY);
}
}
}
void sfx_pause() {
for (int i = 0; i < SFX_MAX; i++) {
if (flags_is(nodes[i].flags, SFX_PLAY | SFX_LOOP)) {
flags_rm(nodes[i].flags, SFX_PLAY);
flags_add(nodes[i].flags, SFX_LOOP_PAUSE);
}
}
}
// Sound effects
sfx_t *sfx_get_node(sfx_source_t source_index) {
error_if(source_index < 0 || source_index > num_sources, "Invalid audio source");
sfx_t *sfx = NULL;
for (int i = 0; i < SFX_MAX; i++) {
if (flags_none(nodes[i].flags, SFX_PLAY | SFX_RESERVE)){
sfx = &nodes[i];
break;
}
}
if (!sfx) {
for (int i = 0; i < SFX_MAX; i++) {
if (flags_not(nodes[i].flags, SFX_RESERVE)) {
sfx = &nodes[i];
break;
}
}
}
error_if(!sfx, "All audio nodes reserved");
flags_set(sfx->flags, SFX_NONE);
sfx->source = source_index;
sfx->volume = 1;
sfx->current_volume = 1;
sfx->pan = 0;
sfx->current_pan = 0;
sfx->position = 0;
// Set default pitch. All voice samples are 44khz,
// other effects 22khz
sfx->pitch = source_index >= SFX_VOICE_MINES ? 1.0 : 0.5;
return sfx;
}
sfx_t *sfx_play(sfx_source_t source_index) {
sfx_t *sfx = sfx_get_node(source_index);
flags_set(sfx->flags, SFX_PLAY);
return sfx;
}
sfx_t *sfx_play_at(sfx_source_t source_index, vec3_t pos, vec3_t vel, float volume) {
sfx_t *sfx = sfx_get_node(source_index);
sfx_set_position(sfx, pos, vel, volume);
if (sfx->volume > 0) {
flags_set(sfx->flags, SFX_PLAY);
}
return sfx;
}
sfx_t *sfx_reserve_loop(sfx_source_t source_index) {
sfx_t *sfx = sfx_get_node(source_index);
flags_set(sfx->flags, SFX_RESERVE | SFX_LOOP | SFX_PLAY);
sfx->volume = 0;
sfx->current_volume = 0;
sfx->current_pan = 0;
sfx->position = rand_float(0, sources[source_index].len);
return sfx;
}
void sfx_set_position(sfx_t *sfx, vec3_t pos, vec3_t vel, float volume) {
vec3_t relative_position = vec3_sub(g.camera.position, pos);
vec3_t relative_velocity = vec3_sub(g.camera.real_velocity, vel);
float distance = vec3_len(relative_position);
sfx->volume = clamp(scale(distance, 512, 32768, 1, 0), 0, 1) * volume;
sfx->pan = -sin(atan2(g.camera.position.x - pos.x, g.camera.position.z - pos.z)+g.camera.angle.y);
// Doppler effect
float away = vec3_dot(relative_velocity, relative_position) / distance;
sfx->pitch = (262144.0 - away) / 524288.0;
}
// Music
uint32_t sfx_music_decode_frame() {
if (!music->file) {
return 0;
}
music->buffer_len = fread(music->buffer, 1, qoa_max_frame_size(&music->qoa), music->file);
uint32_t frame_len;
qoa_decode_frame(music->buffer, music->buffer_len, &music->qoa, music->sample_data, &frame_len);
music->sample_data_pos = 0;
music->sample_data_len = frame_len;
return frame_len;
}
void sfx_music_rewind() {
fseek(music->file, music->first_frame_pos, SEEK_SET);
music->sample_data_len = 0;
music->sample_data_pos = 0;
}
void sfx_music_open(char *path) {
if (music->file) {
fclose(music->file);
music->file = NULL;
}
FILE *file = fopen(path, "rb");
if (!file) {
return;
}
uint8_t header[QOA_MIN_FILESIZE];
int read = fread(header, QOA_MIN_FILESIZE, 1, file);
if (!read) {
fclose(file);
return;
}
qoa_desc qoa;
uint32_t first_frame_pos = qoa_decode_header(header, QOA_MIN_FILESIZE, &qoa);
if (!first_frame_pos) {
fclose(file);
return;
}
fseek(file, first_frame_pos, SEEK_SET);
if (qoa.channels != music->qoa.channels) {
fclose(file);
return;
}
music->qoa = qoa;
music->first_frame_pos = first_frame_pos;
music->file = file;
music->sample_data_len = 0;
music->sample_data_pos = 0;
}
void sfx_music_play(uint32_t index) {
error_if(index >= len(def.music), "Invalid music index");
if (index == music->track_index) {
sfx_music_rewind();
return;
}
printf("open music track %d\n", index);
music->track_index = index;
sfx_music_open(def.music[index].path);
}
void sfx_music_mode(sfx_music_mode_t mode) {
music->mode = mode;
}
// Mixing
void sfx_set_external_mix_cb(void (*cb)(float *, uint32_t len)) {
external_mix_cb = cb;
}
void sfx_stero_mix(float *buffer, uint32_t len) {
if (external_mix_cb) {
external_mix_cb(buffer, len);
return;
}
// Find currently active nodes: those that play and have volume > 0
sfx_t *active_nodes[SFX_MAX_ACTIVE];
int active_nodes_len = 0;
for (int n = 0; n < SFX_MAX; n++) {
sfx_t *sfx = &nodes[n];
if (flags_is(sfx->flags, SFX_PLAY) && (sfx->volume > 0 || sfx->current_volume > 0.01)) {
active_nodes[active_nodes_len++] = sfx;
if (active_nodes_len >= SFX_MAX_ACTIVE) {
break;
}
}
}
uint32_t music_src_index = music->sample_data_pos * music->qoa.channels;
for (int i = 0; i < len; i += 2) {
float left = 0;
float right = 0;
// Fill buffer with all active nodes
for (int n = 0; n < active_nodes_len; n++) {
sfx_t *sfx = active_nodes[n];
if (flags_not(sfx->flags, SFX_PLAY)) {
continue;
}
sfx->current_volume = sfx->current_volume * 0.999 + sfx->volume * 0.001;
sfx->current_pan = sfx->current_pan * 0.999 + sfx->pan * 0.001;
sfx_data_t *source = &sources[sfx->source];
float sample = (float)source->samples[(int)sfx->position] / 32768.0;
left += sample * sfx->current_volume * clamp(1.0 - sfx->current_pan, 0, 1);
right += sample * sfx->current_volume * clamp(1.0 + sfx->current_pan, 0, 1);
sfx->position += sfx->pitch;
if (sfx->position >= source->len) {
if (flags_is(sfx->flags, SFX_LOOP)) {
sfx->position = fmod(sfx->position, source->len);
}
else {
flags_rm(sfx->flags, SFX_PLAY);
}
}
}
left *= save.sfx_volume;
right *= save.sfx_volume;
// Mix in music
if (music->mode != SFX_MUSIC_PAUSED && music->file) {
if (music->sample_data_len - music->sample_data_pos == 0) {
if (!sfx_music_decode_frame()) {
if (music->mode == SFX_MUSIC_RANDOM) {
sfx_music_play(rand_int(0, len(def.music)));
}
else if (music->mode == SFX_MUSIC_SEQUENTIAL) {
sfx_music_play((music->track_index + 1) % len(def.music));
}
else if (music->mode == SFX_MUSIC_LOOP) {
sfx_music_rewind();
}
sfx_music_decode_frame();
}
music_src_index = 0;
}
left += (music->sample_data[music_src_index++] / 32768.0) * save.music_volume;
right += (music->sample_data[music_src_index++] / 32768.0) * save.music_volume;
music->sample_data_pos++;
}
buffer[i+0] = left;
buffer[i+1] = right;
}
}

@ -0,0 +1,85 @@
#ifndef SFX_H
#define SFX_H
#include "../types.h"
typedef enum {
SFX_CRUNCH,
SFX_EBOLT,
SFX_ENGINE_INTAKE,
SFX_ENGINE_RUMBLE,
SFX_ENGINE_THRUST,
SFX_EXPLOSION_1,
SFX_EXPLOSION_2,
SFX_IMPACT,
SFX_MENU_MOVE,
SFX_MENU_SELECT,
SFX_MENU_TRANSITION,
SFX_MINE_DROP,
SFX_MISSILE_FIRE,
SFX_ENGINE_REMOTE,
SFX_POWERUP,
SFX_SHIELD,
SFX_SIREN,
SFX_TRACTOR,
SFX_TURBULENCE,
SFX_CROWD,
SFX_VOICE_MINES,
SFX_VOICE_MISSILE,
SFX_VOICE_ROCKETS,
SFX_VOICE_REVCON,
SFX_VOICE_SHOCKWAVE,
SFX_VOICE_SPECIAL,
SFX_VOICE_COUNT_3,
SFX_VOICE_COUNT_2,
SFX_VOICE_COUNT_1,
SFX_VOICE_COUNT_GO,
} sfx_source_t;
typedef enum {
SFX_NONE = 0,
SFX_PLAY = (1<<0),
SFX_RESERVE = (1<<1),
SFX_LOOP = (1<<2),
SFX_LOOP_PAUSE = (1<<3),
} sfx_flags_t;
typedef struct {
sfx_source_t source;
sfx_flags_t flags;
float pan;
float current_pan;
float volume;
float current_volume;
float pitch;
float position;
} sfx_t;
#define SFX_MAX 64
#define SFX_MAX_ACTIVE 16
void sfx_load();
void sfx_stero_mix(float *buffer, uint32_t len);
void sfx_set_external_mix_cb(void (*cb)(float *, uint32_t len));
void sfx_reset();
void sfx_pause();
void sfx_unpause();
sfx_t *sfx_play(sfx_source_t source_index);
sfx_t *sfx_play_at(sfx_source_t source_index, vec3_t pos, vec3_t vel, float volume);
sfx_t *sfx_reserve_loop(sfx_source_t source_index);
void sfx_set_position(sfx_t *sfx, vec3_t pos, vec3_t vel, float volume);
typedef enum {
SFX_MUSIC_PAUSED,
SFX_MUSIC_RANDOM,
SFX_MUSIC_SEQUENTIAL,
SFX_MUSIC_LOOP
} sfx_music_mode_t;
void sfx_music_next();
void sfx_music_play(uint32_t index);
void sfx_music_mode(sfx_music_mode_t);
void sfx_music_pause();
#endif

File diff suppressed because it is too large Load Diff

@ -0,0 +1,168 @@
#ifndef SHIP_H
#define SHIP_H
#include "../types.h"
#include "track.h"
#include "sfx.h"
#define SHIP_IN_TOW (1<< 0)
#define SHIP_VIEW_REMOTE (1<< 1)
#define SHIP_VIEW_INTERNAL (1<< 2)
#define SHIP_DIRECTION_FORWARD (1<< 3)
#define SHIP_FLYING (1<< 4)
#define SHIP_LEFT_SIDE (1<< 5)
#define SHIP_RACING (1<< 6)
#define SHIP_COLL (1<< 7)
#define SHIP_ON_JUNCTION (1<< 8)
#define SHIP_VISIBLE (1<< 9)
#define SHIP_IN_RESCUE (1<<10)
#define SHIP_OVERTAKEN (1<<11)
#define SHIP_JUST_IN_FRONT (1<<12)
#define SHIP_JUNCTION_LEFT (1<<13)
#define SHIP_SHIELDED (1<<14)
#define SHIP_ELECTROED (1<<15)
#define SHIP_REVCONNED (1<<16)
#define SHIP_SPECIALED (1<<17)
// Timings
#define UPDATE_TIME_INITIAL (200.0 * (1.0/30.0))
#define UPDATE_TIME_THREE (150.0 * (1.0/30.0))
#define UPDATE_TIME_RACE_VIEW (100.0 * (1.0/30.0))
#define UPDATE_TIME_TWO (100.0 * (1.0/30.0))
#define UPDATE_TIME_ONE ( 50.0 * (1.0/30.0))
#define UPDATE_TIME_GO ( 0.0 * (1.0/30.0))
// Physics conversion
#define FIXED_TO_FLOAT(V) ((V) * (1.0/4096.0))
#define ANGLE_NORM_TO_RADIAN(V) ((V) * M_PI * 2.0)
#define NTSC_STEP_TO_RATE_PER_SECOND(V) ((V) * 30.0)
#define NTSC_ACCELERATION(V) NTSC_STEP_TO_RATE_PER_SECOND(NTSC_STEP_TO_RATE_PER_SECOND(V))
#define NTSC_VELOCITY(V) NTSC_STEP_TO_RATE_PER_SECOND(V)
#define PITCH_VELOCITY(V) ((V) * (1.0/16.0))
#define YAW_VELOCITY(V) ((V) * (1.0/64.0))
#define ROLL_VELOCITY(V) ((V) * (1.0))
#define SHIP_FLYING_GRAVITY 80000.0
#define SHIP_ON_TRACK_GRAVITY 30000.0
#define SHIP_MIN_RESISTANCE 20 // 12
#define SHIP_MAX_RESISTANCE 74
#define SHIP_VELOCITY_SHIFT 6
#define SHIP_TRACK_MAGNET 64 // 64
#define SHIP_TRACK_FLOAT 256
#define SHIP_PITCH_ACCEL NTSC_ACCELERATION(ANGLE_NORM_TO_RADIAN(FIXED_TO_FLOAT(PITCH_VELOCITY(30))))
#define SHIP_THRUST_RATE NTSC_VELOCITY(16)
#define SHIP_THRUST_FALLOFF NTSC_VELOCITY(8)
#define SHIP_BRAKE_RATE NTSC_VELOCITY(32)
typedef struct ship_t {
int16_t pilot;
int flags;
section_t *section, *prev_section;
vec3_t dir_forward;
vec3_t dir_right;
vec3_t dir_up;
vec3_t position;
vec3_t velocity;
vec3_t acceleration;
vec3_t thrust;
vec3_t angle;
vec3_t angular_velocity;
vec3_t angular_acceleration;
vec3_t temp_target; // used for start position and rescue target
float turn_rate;
float turn_rate_max;
float turn_rate_from_hit;
float mass;
float thrust_mag;
float thrust_max;
float current_thrust_max;
float speed;
float brake_left;
float brake_right;
float resistance;
float skid;
float remote_thrust_mag;
float remote_thrust_max;
// Remote Ship Attributes
int16_t fight_back;
float start_accelerate_timer;
// Weapon Attributes
uint8_t weapon_type;
struct ship_t *weapon_target;
float ebolt_timer;
float ebolt_effect_timer;
float revcon_timer;
float special_timer;
// Race Control Attributes
int position_rank;
int lap;
int max_lap;
float lap_time;
int16_t section_num;
int16_t prev_section_num;
int16_t total_section_num;
float update_timer;
float last_impact_time;
mat4_t mat;
Object *model;
Object *collision_model;
uint16_t shadow_texture;
struct {
vec3_t *v;
vec3_t initial;
} exhaust_plume[3];
// Control Routines
vec3_t (*update_strat_func)(struct ship_t *, track_face_t *);
void (*update_func)(struct ship_t *);
// Audio
sfx_t *sfx_engine_thrust;
sfx_t *sfx_engine_intake;
sfx_t *sfx_turbulence;
sfx_t *sfx_shield;
} ship_t;
void ships_load();
void ships_init(section_t *section);
void ships_draw();
void ships_update();
void ship_init(ship_t *self, section_t *section, int pilot, int position);
void ship_init_exhaust_plume(ship_t *self);
void ship_draw(ship_t *self);
void ship_draw_shadow(ship_t *self);
void ship_update(ship_t *self);
void ship_collide_with_track(ship_t *self, track_face_t *face);
void ship_collide_with_ship(ship_t *self, ship_t *other);
vec3_t ship_cockpit(ship_t *self);
vec3_t ship_nose(ship_t *self);
vec3_t ship_wing_left(ship_t *self);
vec3_t ship_wing_right(ship_t *self);
#endif

@ -0,0 +1,536 @@
#include "../mem.h"
#include "../input.h"
#include "../system.h"
#include "../utils.h"
#include "object.h"
#include "track.h"
#include "ship.h"
#include "weapon.h"
#include "hud.h"
#include "droid.h"
#include "camera.h"
#include "ship_ai.h"
#include "game.h"
vec3_t ship_ai_strat_hold_center(ship_t *self, track_face_t *face);
vec3_t ship_ai_strat_hold_right(ship_t *self, track_face_t *face);
vec3_t ship_ai_strat_hold_left(ship_t *self, track_face_t *face);
vec3_t ship_ai_strat_block(ship_t *self, track_face_t *face);
vec3_t ship_ai_strat_avoid(ship_t *self, track_face_t *face);
vec3_t ship_ai_strat_avoid_other(ship_t *self, track_face_t *face);
vec3_t ship_ai_strat_zig_zag(ship_t *self, track_face_t *face);
void ship_ai_update_intro(ship_t *self) {
self->temp_target = self->position;
self->update_func = ship_ai_update_intro_await_go;
self->sfx_engine_thrust = sfx_reserve_loop(SFX_ENGINE_REMOTE);
sfx_set_position(self->sfx_engine_thrust, self->position, self->velocity, 0.1);
}
void ship_ai_update_intro_await_go(ship_t *self) {
self->position.y = self->temp_target.y + sin(self->update_timer * (80.0 + self->pilot * 3.0) * 30.0 * M_PI * 2.0 / 4096.0) * 32;
self->update_timer -= system_tick();
if (self->update_timer <= UPDATE_TIME_GO) {
self->update_func = ship_ai_update_race;
}
}
vec3_t ship_ai_strat_hold_left(ship_t *self, track_face_t *face) {
vec3_t fv1 = face->tris[0].vertices[1].pos;
vec3_t fv2 = face->tris[0].vertices[0].pos;
return vec3_mulf(vec3_sub(fv1, fv2), 0.5);
}
vec3_t ship_ai_strat_hold_right(ship_t *self, track_face_t *face) {
vec3_t fv1 = face->tris[0].vertices[0].pos;
vec3_t fv2 = face->tris[0].vertices[1].pos;
return vec3_mulf(vec3_sub(fv1, fv2), 0.5);
}
vec3_t ship_ai_strat_hold_center(ship_t *self, track_face_t *face) {
return vec3(0, 0, 0);
}
vec3_t ship_ai_strat_block(ship_t *self, track_face_t *face) {
if (flags_is(g.ships[g.pilot].flags, SHIP_LEFT_SIDE)) {
return ship_ai_strat_hold_left(self, face);
}
else {
return ship_ai_strat_hold_right(self, face);
}
}
vec3_t ship_ai_strat_avoid(ship_t *self, track_face_t *face) {
if (flags_is(g.ships[g.pilot].flags, SHIP_LEFT_SIDE)) {
return ship_ai_strat_hold_right(self, face);
}
else {
return ship_ai_strat_hold_left(self, face);
}
}
vec3_t ship_ai_strat_avoid_other(ship_t *self, track_face_t *face) {
int min_section_num = 100;
ship_t *avoid_ship;
for (int i = 0; i < NUM_PILOTS; i++) {
if (i != self->pilot) {
int section_diff = g.ships[i].total_section_num - self->total_section_num;
if (min_section_num < section_diff) {
min_section_num = section_diff;
avoid_ship = &g.ships[i];
}
}
}
if (avoid_ship && min_section_num < 10 && min_section_num > -2) {
if (flags_is(avoid_ship->flags, SHIP_LEFT_SIDE)) {
return ship_ai_strat_hold_right(self, face);
}
else {
return ship_ai_strat_hold_left(self, face);
}
}
return vec3(0, 0, 0);
}
vec3_t ship_ai_strat_zig_zag(ship_t *self, track_face_t *face) {
int update_count = (self->update_timer * 30)/50;
if (update_count % 2) {
return ship_ai_strat_hold_right(self, face);
}
else {
return ship_ai_strat_hold_left(self, face);
}
}
void ship_ai_update_race(ship_t *self) {
vec3_t offset_vector = vec3(0, 0, 0);
ship_t *player = &(g.ships[g.pilot]);
if (self->ebolt_timer > 0) {
self->ebolt_timer -= system_tick();
}
if (self->ebolt_timer <= 0) {
flags_rm(self->flags, SHIP_ELECTROED);
}
int behind_speed = def.circuts[g.circut].settings[g.race_class].behind_speed;
if (flags_not(self->flags, SHIP_FLYING)) {
// Find First track base section
track_face_t *face = track_section_get_base_face(self->section);
int section_diff = self->total_section_num - player->total_section_num;
flags_rm(self->flags, SHIP_JUST_IN_FRONT);
if (self == player) {
self->update_strat_func = ship_ai_strat_avoid_other;
if (self->remote_thrust_max > self->speed) {
self->speed += self->remote_thrust_mag * 30 * system_tick();
}
}
else {
// Make global DPA decisions , these will effect the craft in
// relation to your race position
// Accelerate remote ships away at start, start_accelerate_count set in
// InitShipData and is an exponential progression
if (self->start_accelerate_timer > 0) {
self->start_accelerate_timer -= system_tick();
self->update_timer = 0;
self->update_strat_func = ship_ai_strat_avoid;
if ((self->remote_thrust_max + 1200) > self->speed) {
self->speed += (self->remote_thrust_mag + 150) * 30 * system_tick();
}
}
// Ship has been left WELL BEHIND; set it to avoid
// other ships and update its speed as normal
else if (section_diff < -10) { // Ship behind, AVOID
self->update_timer = 0;
self->update_strat_func = ship_ai_strat_avoid;
// If ship has been well passed, increase its speed to allow
// it to make a challenge when the player fouls up
if (((self->remote_thrust_max + behind_speed) > self->speed)) {
self->speed += self->remote_thrust_mag * 30 * system_tick();
}
}
// Ship is JUST AHEAD
else if ((section_diff <= 4) && (section_diff > 0)) { // Ship close by, beware does not account for lapped opponents yet
flags_add(self->flags, SHIP_JUST_IN_FRONT);
if (self->update_timer <= 0) { // Make New Decision
int chance = rand_int(0, 64); // 12
self->update_timer = UPDATE_TIME_JUST_FRONT;
if (self->fight_back) { // Ship wants to make life difficult
if ((chance < 40) || (self->weapon_type == WEAPON_TYPE_NONE)) { // Ship will try to block you
self->update_strat_func = ship_ai_strat_block;
}
else if ((chance >= 40) && (chance < 52)) { // Ship will attempt to drop mines in your path
self->update_strat_func = ship_ai_strat_block;
if (flags_not(self->flags, SHIP_SHIELDED) && flags_is(self->flags, SHIP_RACING)) {
sfx_play(SFX_VOICE_MINES);
self->weapon_type = WEAPON_TYPE_MINE;
weapons_fire_delayed(self, self->weapon_type);
}
}
else if ((chance >= 52) && (chance < 64)) { // Ship will raise its shield
self->update_strat_func = ship_ai_strat_block;
if (flags_not(self->flags, SHIP_SHIELDED)) {
self->weapon_type = WEAPON_TYPE_SHIELD;
weapons_fire(self, self->weapon_type);
}
}
}
else { // Let the first ships be easy to pass
self->update_strat_func = ship_ai_strat_avoid;
}
}
self->update_timer -= system_tick();
if (flags_is(self->flags, SHIP_OVERTAKEN)) {
// If ship has just overtaken, slow it down to a reasonable speed
if ((self->remote_thrust_max + behind_speed) > self->speed) {
self->speed += self->remote_thrust_mag * 30 * system_tick();
}
}
else {
// Increase the speed of any craft just in front slightly
if (((self->remote_thrust_max + (behind_speed >> 1)) > self->speed)) {
self->speed += self->remote_thrust_mag * 30 * system_tick();
}
}
}
// Ship is JUST BEHIND; we must decided if and how many times it 'should have a go back'
else if ((section_diff >= -10) && (section_diff <= 0)) { // Ship just behind, MAKE DECISION
if (self->update_timer <= 0) { // Make New Decision
self->update_timer = UPDATE_TIME_JUST_BEHIND;
if (self->fight_back) { // Ship wants you to say "Outside Now!"
if (self->weapon_type == WEAPON_TYPE_NONE) {
self->update_strat_func = ship_ai_strat_avoid;
flags_add(self->flags, SHIP_OVERTAKEN);
}
else {
int chance = rand_int(0, 64);
if (chance < 48) {
self->update_strat_func = ship_ai_strat_block;
}
else if ((chance >= 40) && (chance < 54)) {
self->update_strat_func = ship_ai_strat_avoid;
flags_rm(self->flags, SHIP_OVERTAKEN);
if (flags_not(self->flags, SHIP_SHIELDED) && flags_is(self->flags, SHIP_RACING)) {
sfx_play(SFX_VOICE_ROCKETS);
self->weapon_type = WEAPON_TYPE_ROCKET;
weapons_fire_delayed(self, self->weapon_type);
}
}
else if ((chance >= 54) && (chance < 60)) {
self->update_strat_func = ship_ai_strat_avoid;
flags_rm(self->flags, SHIP_OVERTAKEN);
if (flags_not(self->flags, SHIP_SHIELDED) && flags_is(self->flags, SHIP_RACING)) {
sfx_play(SFX_VOICE_MISSILE);
self->weapon_type = WEAPON_TYPE_MISSILE;
self->weapon_target = &g.ships[g.pilot];
weapons_fire_delayed(self, self->weapon_type);
}
}
else if ((chance >= 60) && (chance < 64)) {
self->update_strat_func = ship_ai_strat_avoid;
flags_rm(self->flags, SHIP_OVERTAKEN);
if (flags_not(self->flags, SHIP_SHIELDED) && flags_is(self->flags, SHIP_RACING)) {
sfx_play(SFX_VOICE_SHOCKWAVE);
self->weapon_type = WEAPON_TYPE_EBOLT;
self->weapon_target = &g.ships[g.pilot];
weapons_fire_delayed(self, self->weapon_type);
}
}
}
}
else { // If ship destined to be tail-ender then slow down
self->remote_thrust_max = 2100 ;
self->remote_thrust_mag = 25;
self->speed = 2100 ;
self->update_strat_func = ship_ai_strat_avoid;
flags_rm(self->flags, SHIP_OVERTAKEN);
}
}
for (int i = 0; i < NUM_PILOTS; i++) { // If another ship is just in front pass fight on
if (flags_is(g.ships[i].flags, SHIP_JUST_IN_FRONT)) {
self->update_strat_func = ship_ai_strat_avoid;
flags_rm(self->flags, SHIP_OVERTAKEN);
}
}
self->update_timer -= system_tick();
if (flags_is(self->flags, SHIP_OVERTAKEN)) {
if ((self->remote_thrust_max + 700) > self->speed) {
self->speed += self->remote_thrust_mag * 2 * 30 * system_tick();
}
}
else {
if (((self->remote_thrust_max + behind_speed) > self->speed)) {
self->speed += self->remote_thrust_mag * 30 * system_tick();
}
}
}
// Ship is WELL AHEAD; we must slow the opponent to
// give the weaker player a chance to catch up
else if (section_diff > (NUM_PILOTS - self->position_rank) * 15 && section_diff < 150) {
self->speed += self->remote_thrust_mag * 0.5 * 30 * system_tick();
if (self->speed > self->remote_thrust_max * 0.5) {
self->speed = self->remote_thrust_max * 0.5;
}
self->update_timer = 0;
self->update_strat_func = ship_ai_strat_hold_center;
}
// Ship is TOO FAR AHEAD
else if (section_diff >= 150) { // Ship too far ahead, let it continue
self->update_timer = 0;
self->update_strat_func = ship_ai_strat_avoid;
if ((self->remote_thrust_max > self->speed)) {
self->speed += self->remote_thrust_mag * 30 * system_tick();
}
}
// Ship is IN SIGHT
else if ((section_diff <= 10) && (section_diff > 4)) { // Ship close by, beware does not account for lapped opponents yet
if (self->update_timer <= 0) { // Make New Decision
int chance = rand_int(0, 5);
self->update_timer = UPDATE_TIME_IN_SIGHT;
switch (chance) {
case 0: self->update_strat_func = ship_ai_strat_hold_center; break;
case 1: self->update_strat_func = ship_ai_strat_hold_left; break;
case 2: self->update_strat_func = ship_ai_strat_hold_right; break;
case 3: self->update_strat_func = ship_ai_strat_block; break;
case 4: self->update_strat_func = ship_ai_strat_zig_zag; break;
}
}
self->update_timer -= system_tick();
if ((self->remote_thrust_max > self->speed)) {
self->speed += self->remote_thrust_mag * 30 * system_tick();
}
} // End of DPA control options
// Ship is JUST OUT OF SIGHT
else {
self->update_timer = 0;
self->update_strat_func = ship_ai_strat_hold_center;
if ((self->remote_thrust_max > self->speed)) {
self->speed += self->remote_thrust_mag * 30 * system_tick();
}
}
}
if (!self->update_strat_func) {
self->update_strat_func = ship_ai_strat_hold_center;
}
offset_vector = (self->update_strat_func)(self, face);
// Make decision as to which path the craft will take at a junction
section_t *section = self->section->prev;
for (int i = 0; i < 4; i++) {
section = section->next;
}
if (section->junction) {
if (flags_is(section->junction->flags, SECTION_JUNCTION_START)) {
int chance = rand_int(0, 2);
if (chance == 0) {
flags_add(self->flags, SHIP_JUNCTION_LEFT);
}
else {
flags_rm(self->flags, SHIP_JUNCTION_LEFT);
}
}
}
section = self->section->prev;
for (int i = 0; i < 4; i++) {
if (section->junction) {
if (flags_is(section->junction->flags, SECTION_JUNCTION_START)) {
if (flags_is(self->flags, SHIP_JUNCTION_LEFT)) {
section = section->junction;
}
else {
section = section->next;
}
}
else {
section = section->next;
}
}
else {
section = section->next;
}
}
section_t *next = section->next;
section = self->section;
// General routines - Non decision based
// Bleed off speed as orientation changes
self->speed -= fabsf(self->speed * self->angular_velocity.y) * 4 / (M_PI * 2) * system_tick(); // >> 14
self->speed -= fabsf(self->speed * self->angular_velocity.x) * 4 / (M_PI * 2) * system_tick(); // >> 14
// If remote has gone over boost
if (flags_is(face->flags, FACE_BOOST) && (self->update_strat_func == ship_ai_strat_hold_left || self->update_strat_func == ship_ai_strat_hold_center)) {
self->speed += 200 * 30 * system_tick();
}
face++;
if (flags_is(face->flags, FACE_BOOST) && (self->update_strat_func == ship_ai_strat_hold_right || self->update_strat_func == ship_ai_strat_hold_center)) {
self->speed += 200 * 30 * system_tick();
}
vec3_t track_target;
if (flags_is(self->section->flags, SECTION_JUMP)) { // Cure for ships getting stuck on hump lip
track_target = vec3_sub(self->section->center, self->section->prev->center);
}
else {
track_target = vec3_sub(next->center, section->center);
}
float gap_length = vec3_len(track_target);
track_target = vec3_mulf(track_target, self->speed / gap_length);
vec3_t path1 = vec3_add(section->center, offset_vector);
vec3_t path2 = vec3_add(next->center, offset_vector);
vec3_t best_path = vec3_project_to_ray(self->position, path2, path1);
self->acceleration = vec3_add(track_target, vec3_mulf(vec3_sub(best_path, self->position), 0.5));
vec3_t face_point = face->tris[0].vertices[0].pos;
float height = vec3_distance_to_plane(self->position, face_point, face->normal);
if (height < 50) {
height = 50;
}
self->acceleration = vec3_add(self->acceleration, vec3_mulf(vec3_sub(
vec3_mulf(face->normal, (SHIP_TRACK_FLOAT * SHIP_TRACK_MAGNET) / height),
vec3_mulf(face->normal, SHIP_TRACK_MAGNET)
), 16.0));
self->velocity = vec3_add(self->velocity, vec3_mulf(self->acceleration, 30 * system_tick()));
float xy_dist = sqrt(track_target.x * track_target.x + track_target.z * track_target.z);
self->angular_velocity.x = wrap_angle(-atan2(track_target.y, xy_dist) - self->angle.x) * (1.0/16.0) * 30;
self->angular_velocity.y = (wrap_angle(-atan2(track_target.x, track_target.z) - self->angle.y) * (1.0/16.0)) * 30 + self->turn_rate_from_hit;
}
// Ship is SHIP_FLYING
else {
section_t *section = self->section->next->next;
section_t *next = section->next;
self->update_strat_func = ship_ai_strat_hold_center;
offset_vector = (self->update_strat_func)(self, NULL);
if (self->remote_thrust_max > self->speed) {
self->speed += self->remote_thrust_mag ;
}
self->speed -= fabsf(self->speed * self->angular_velocity.y) * (4 * M_PI * 2) * system_tick();
vec3_t track_target = vec3_sub(next->center, section->center);
float gap_length = vec3_len(track_target);
track_target.x = (track_target.x * self->speed) / gap_length;
track_target.z = (track_target.z * self->speed) / gap_length;
track_target.y = 500;
vec3_t best_path = vec3_project_to_ray(self->position, next->center, self->section->center);
self->acceleration = vec3(
(track_target.x + ((best_path.x - self->position.x) * 0.5)),
track_target.y,
(track_target.z + ((best_path.z - self->position.z) * 0.5))
);
self->velocity = vec3_add(self->velocity, vec3_mulf(self->acceleration, 30 * system_tick()));
self->angular_velocity.x = -0.3 - self->angle.x * 30;
self->angular_velocity.y = wrap_angle(-atan2(track_target.x, track_target.z) - self->angle.y) * (1.0/16.0) * 30;
}
self->angular_velocity.z += (self->angular_velocity.y * 2.0 - self->angular_velocity.z * 0.5) * 30 * system_tick();
self->turn_rate_from_hit -= self->turn_rate_from_hit * 0.125 * 30 * system_tick();
self->angle = vec3_add(self->angle, vec3_mulf(self->angular_velocity, system_tick()));
self->angle.z -= self->angle.z * 0.125 * 30 * system_tick();
self->angle = vec3_wrap_angle(self->angle);
self->velocity = vec3_sub(self->velocity, vec3_mulf(self->velocity, 0.125 * 30 * system_tick()));
self->position = vec3_add(self->position, vec3_mulf(self->velocity, 0.015625 * 30 * system_tick()));
if (flags_is(self->flags, SHIP_ELECTROED)) {
self->position = vec3_add(self->position, vec3(rand_float(-20, 20), rand_float(-20, 20), rand_float(-20, 20)));
if (rand_int(0, 50) == 0) {
self->speed -= self->speed * 0.5 * 30 * system_tick();
}
}
sfx_set_position(self->sfx_engine_thrust, self->position, self->velocity, 0.5);
}

@ -0,0 +1,14 @@
#ifndef SHIP_AI_H
#define SHIP_AI_H
#include "ship.h"
#define UPDATE_TIME_JUST_FRONT (150.0 * (1.0/30.0))
#define UPDATE_TIME_JUST_BEHIND (200.0 * (1.0/30.0))
#define UPDATE_TIME_IN_SIGHT (200.0 * (1.0/30.0))
void ship_ai_update_race(ship_t *self);
void ship_ai_update_intro(ship_t *self);
void ship_ai_update_intro_await_go(ship_t *self);
#endif

@ -0,0 +1,582 @@
#include <stdint.h>
#include <math.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include "../mem.h"
#include "object.h"
#include "track.h"
#include "ship.h"
#include "weapon.h"
#include "hud.h"
#include "droid.h"
#include "camera.h"
#include "../utils.h"
#include "scene.h"
#include "../input.h"
#include "../system.h"
#include "sfx.h"
#include "ship_player.h"
#include "ship_ai.h"
#include "game.h"
#include "particle.h"
void ship_player_update_sfx(ship_t *self) {
float speedf = self->speed * 0.000015;
self->sfx_engine_intake->volume = clamp(speedf, 0, 0.5);
self->sfx_engine_intake->pitch = 0.5 + speedf * 1.25;
self->sfx_engine_thrust->volume = 0.05 + 0.025 * (self->thrust_mag / self->thrust_max);
self->sfx_engine_thrust->pitch = 0.2 + 0.5 * (self->thrust_mag / self->thrust_max) + speedf;
float brake_left = self->brake_left * 0.0035;
float brake_right = self->brake_right * 0.0035;
self->sfx_turbulence->volume = (speedf * brake_left + speedf * brake_right) * 0.5;
self->sfx_turbulence->pan = (brake_right - brake_left);
self->sfx_shield->volume = flags_is(self->flags, SHIP_SHIELDED) ? 0.5 : 0;
}
void ship_player_update_intro(ship_t *self) {
self->temp_target = self->position;
self->sfx_engine_thrust = sfx_reserve_loop(SFX_ENGINE_THRUST);
self->sfx_engine_intake = sfx_reserve_loop(SFX_ENGINE_INTAKE);
self->sfx_shield = sfx_reserve_loop(SFX_SHIELD);
self->sfx_turbulence = sfx_reserve_loop(SFX_TURBULENCE);
ship_player_update_intro_general(self);
self->update_func = ship_player_update_intro_await_three;
}
void ship_player_update_intro_await_three(ship_t *self) {
ship_player_update_intro_general(self);
if (self->update_timer <= UPDATE_TIME_THREE) {
sfx_t *sfx = sfx_play(SFX_VOICE_COUNT_3);
self->update_func = ship_player_update_intro_await_two;
}
}
void ship_player_update_intro_await_two(ship_t *self) {
ship_player_update_intro_general(self);
if (self->update_timer <= UPDATE_TIME_TWO) {
scene_set_start_booms(1);
sfx_t *sfx = sfx_play(SFX_VOICE_COUNT_2);
self->update_func = ship_player_update_intro_await_one;
}
}
void ship_player_update_intro_await_one(ship_t *self) {
ship_player_update_intro_general(self);
if (self->update_timer <= UPDATE_TIME_ONE) {
scene_set_start_booms(2);
sfx_t *sfx = sfx_play(SFX_VOICE_COUNT_1);
self->update_func = ship_player_update_intro_await_go;
}
}
void ship_player_update_intro_await_go(ship_t *self) {
ship_player_update_intro_general(self);
if (self->update_timer <= UPDATE_TIME_GO) {
scene_set_start_booms(3);
sfx_t *sfx = sfx_play(SFX_VOICE_COUNT_GO);
if (flags_is(self->flags, SHIP_RACING)) {
// Check for stall
if (self->thrust_mag >= 680 && self->thrust_mag <= 700) {
self->thrust_mag = 1800;
self->current_thrust_max = 1800;
}
else if (self->thrust_mag < 680) {
self->current_thrust_max = self->thrust_max;
}
else {
self->current_thrust_max = 200;
}
self->update_timer = UPDATE_TIME_STALL;
self->update_func = ship_player_update_race;
}
else {
self->update_func = ship_ai_update_race;
}
}
}
void ship_player_update_intro_general(ship_t *self) {
self->update_timer -= system_tick();
self->position.y = self->temp_target.y + sin(self->update_timer * 80.0 * 30.0 * M_PI * 2.0 / 4096.0) * 32;
// Thrust
if (input_state(A_THRUST)) {
self->thrust_mag += input_state(A_THRUST) * SHIP_THRUST_RATE * system_tick();
}
else {
self->thrust_mag -= SHIP_THRUST_RATE * system_tick();
}
self->thrust_mag = clamp(self->thrust_mag, 0, self->thrust_max);
// View
if (input_pressed(A_CHANGE_VIEW)) {
if (flags_not(self->flags, SHIP_VIEW_INTERNAL)) {
g.camera.update_func = camera_update_race_internal;
flags_add(self->flags, SHIP_VIEW_INTERNAL);
}
else {
g.camera.update_func = camera_update_race_external;
flags_rm(self->flags, SHIP_VIEW_INTERNAL);
}
}
ship_player_update_sfx(self);
}
void ship_player_update_race(ship_t *self) {
if (flags_not(self->flags, SHIP_RACING)) {
self->update_func = ship_ai_update_race;
return;
}
if (self->ebolt_timer > 0) {
self->ebolt_timer -= system_tick();
}
if (self->ebolt_timer <= 0) {
flags_rm(self->flags, SHIP_ELECTROED);
}
if (self->revcon_timer > 0) {
self->revcon_timer -= system_tick();
}
if (self->revcon_timer <= 0) {
flags_rm(self->flags, SHIP_REVCONNED);
}
if (self->special_timer > 0) {
self->special_timer -= system_tick();
}
if (self->special_timer <= 0) {
flags_rm(self->flags, SHIP_SPECIALED);
}
if (flags_is(self->flags, SHIP_REVCONNED)) {
// FIXME_PL: make sure revconned is honored
}
self->angular_acceleration = vec3(0, 0, 0);
if (input_state(A_LEFT)) {
if (self->angular_velocity.y >= 0) {
self->angular_acceleration.y += input_state(A_LEFT) * self->turn_rate;
}
else if (self->angular_velocity.y < 0) {
self->angular_acceleration.y += input_state(A_LEFT) * self->turn_rate * 2;
}
}
else if (input_state(A_RIGHT)) {
if (self->angular_velocity.y <= 0) {
self->angular_acceleration.y -= input_state(A_RIGHT) * self->turn_rate;
}
else if (self->angular_velocity.y > 0) {
self->angular_acceleration.y -= input_state(A_RIGHT) * self->turn_rate * 2;
}
}
if (flags_is(self->flags, SHIP_ELECTROED)) {
self->ebolt_effect_timer += system_tick();
// Yank the ship every 0.1 seconds
if (self->ebolt_effect_timer > 0.1) {
if (flags_is(self->flags, SHIP_VIEW_INTERNAL)) {
// SetShake(2); // FIXME
}
self->angular_velocity.y += rand_float(-0.5, 0.5); // FIXME: 60fps
self->ebolt_effect_timer -= 0.1;
}
}
self->angular_acceleration.x += input_state(A_DOWN) * SHIP_PITCH_ACCEL;
self->angular_acceleration.x -= input_state(A_UP) * SHIP_PITCH_ACCEL;
// Handle Stall
if (self->update_timer > 0) {
if (self->current_thrust_max < 500) {
self->current_thrust_max += rand_float(0, 165) * system_tick();
}
self->update_timer -= system_tick();
}
else {
// End stall / boost
self->current_thrust_max = self->thrust_max;
}
// Thrust
if (input_state(A_THRUST)) {
self->thrust_mag += input_state(A_THRUST) * SHIP_THRUST_RATE * system_tick();
}
else {
self->thrust_mag -= SHIP_THRUST_FALLOFF * system_tick();
}
self->thrust_mag = clamp(self->thrust_mag, 0, self->current_thrust_max);
if (flags_is(self->flags, SHIP_ELECTROED) && rand_int(0, 80) == 0) {
self->thrust_mag -= self->thrust_mag * 0.25; // FIXME: 60fps
}
// Brake
if (input_state(A_BRAKE_RIGHT)) {
self->brake_right += SHIP_BRAKE_RATE * system_tick();
}
else if (self->brake_right > 0) {
self->brake_right -= SHIP_BRAKE_RATE * system_tick();
}
self->brake_right = clamp(self->brake_right, 0, 256);
if (input_state(A_BRAKE_LEFT)) {
self->brake_left += SHIP_BRAKE_RATE * system_tick();
}
else if (self->brake_left > 0) {
self->brake_left -= SHIP_BRAKE_RATE * system_tick();
}
self->brake_left = clamp(self->brake_left, 0, 256);
// View
if (input_pressed(A_CHANGE_VIEW)) {
if (flags_not(self->flags, SHIP_VIEW_INTERNAL)) {
g.camera.update_func = camera_update_race_internal;
flags_add(self->flags, SHIP_VIEW_INTERNAL);
}
else {
g.camera.update_func = camera_update_race_external;
flags_rm(self->flags, SHIP_VIEW_INTERNAL);
}
}
if (self->weapon_type == WEAPON_TYPE_MISSILE || self->weapon_type == WEAPON_TYPE_EBOLT) {
self->weapon_target = ship_player_find_target(self);
}
else {
self->weapon_target = NULL;
}
// Fire
// self->weapon_type = WEAPON_TYPE_MISSILE; // Test weapon
if (input_pressed(A_FIRE) && self->weapon_type != WEAPON_TYPE_NONE) {
if (flags_not(self->flags, SHIP_SHIELDED)) {
weapons_fire(self, self->weapon_type);
}
else {
sfx_play(SFX_MENU_MOVE);
}
}
// Physics
// Calculate thrust vector along principle axis of ship
self->thrust = vec3_mulf(self->dir_forward, self->thrust_mag * 64);
self->speed = vec3_len(self->velocity);
vec3_t forward_velocity = vec3_mulf(self->dir_forward, self->speed);
// SECTION_JUMP
if (flags_is(self->section->flags, SECTION_JUMP)) {
track_face_t *face = track_section_get_base_face(self->section);
// Project the ship's position to the track section using the face normal.
// If the point lands on the track, the sum of the angles between the
// point and the track vertices will be M_PI*2.
// If it's less then M_PI*2 (minus a safety margin) we are flying!
vec3_t face_point = face->tris[0].vertices[0].pos;
float height = vec3_distance_to_plane(self->position, face_point, face->normal);
vec3_t plane_point = vec3_sub(self->position, vec3_mulf(face->normal, height));
vec3_t vec0 = vec3_sub(plane_point, face->tris[0].vertices[1].pos);
vec3_t vec1 = vec3_sub(plane_point, face->tris[0].vertices[2].pos);
face++;
vec3_t vec2 = vec3_sub(plane_point, face->tris[0].vertices[0].pos);
vec3_t vec3 = vec3_sub(plane_point, face->tris[1].vertices[0].pos);
float angle =
vec3_angle(vec0, vec2) +
vec3_angle(vec2, vec3) +
vec3_angle(vec3, vec1) +
vec3_angle(vec1, vec0);
if (angle < M_PI * 2 - 0.01) {
flags_add(self->flags, SHIP_FLYING);
}
}
// Held by track
if (flags_not(self->flags, SHIP_FLYING)) {
track_face_t *face = track_section_get_base_face(self->section);
ship_collide_with_track(self, face);
if (flags_not(self->flags, SHIP_LEFT_SIDE)) {
face++;
}
// Boost
if (flags_not(self->flags, SHIP_SPECIALED) && flags_is(face->flags, FACE_BOOST)) {
vec3_t track_direction = vec3_sub(self->section->next->center, self->section->center);
self->velocity = vec3_add(self->velocity, vec3_mulf(track_direction, 30 * system_tick()));
}
vec3_t face_point = face->tris[0].vertices[0].pos;
float height = vec3_distance_to_plane(self->position, face_point, face->normal);
// Collision with floor
if (height <= 0) {
if (self->last_impact_time > 0.2) {
self->last_impact_time = 0;
sfx_play_at(SFX_IMPACT, self->position, vec3(0,0,0), 1);
}
self->velocity = vec3_reflect(self->velocity, face->normal, 2);
self->velocity = vec3_sub(self->velocity, vec3_mulf(self->velocity, 0.125));
self->velocity = vec3_sub(self->velocity, face->normal);
}
else if (height < 30) {
self->velocity = vec3_add(self->velocity, face->normal);
}
if (height < 50) {
height = 50;
}
// Calculate acceleration
float brake = (self->brake_left + self->brake_right);
float resistance = (self->resistance * (SHIP_MAX_RESISTANCE - (brake * 0.125))) * 0.0078125;
vec3_t force = vec3(0, SHIP_ON_TRACK_GRAVITY, 0);
force = vec3_add(force, vec3_mulf(vec3_mulf(face->normal, 4096), (SHIP_TRACK_MAGNET * SHIP_TRACK_FLOAT) / height));
force = vec3_sub(force, vec3_mulf(vec3_mulf(face->normal, 4096), SHIP_TRACK_MAGNET));
force = vec3_add(force, self->thrust);
self->acceleration = vec3_divf(vec3_sub(forward_velocity, self->velocity), self->skid + brake * 0.25);
self->acceleration = vec3_add(self->acceleration, vec3_divf(force, self->mass));
self->acceleration = vec3_sub(self->acceleration, vec3_divf(self->velocity, resistance));
// Burying the nose in the track? Move it out!
vec3_t nose_pos = vec3_add(self->position, vec3_mulf(self->dir_forward, 128));
float nose_height = vec3_distance_to_plane(nose_pos,face_point, face->normal);
if (nose_height < 600) {
self->angular_acceleration.x += NTSC_ACCELERATION(ANGLE_NORM_TO_RADIAN(FIXED_TO_FLOAT((height - nose_height + 5) * (1.0/16.0))));
}
else {
self->angular_acceleration.x += NTSC_ACCELERATION(ANGLE_NORM_TO_RADIAN(FIXED_TO_FLOAT(-50.0/16.0)));
}
}
// Flying
else {
// Detect the need for a rescue droid
section_t *next = self->section->next;
vec3_t best_path = vec3_project_to_ray(self->position, next->center, self->section->center);
vec3_t distance = vec3_sub(best_path, self->position);
if (distance.y > 0) {
distance.y = distance.y * 0.0001;
}
else {
distance = vec3_mulf(distance, 8);
}
// Do we need to be rescued?
if (vec3_len(distance) > 8000) {
self->update_func = ship_player_update_rescue;
self->update_timer = UPDATE_TIME_RESCUE;
flags_add(self->flags, SHIP_IN_RESCUE | SHIP_FLYING);
section_t *landing = self->section->prev;
for (int i = 0; i < 3; i++) {
landing = landing->prev;
}
for (int i = 0; i < 10 && flags_not(landing->flags, SECTION_JUMP); i++) {
landing = landing->next;
}
self->section = landing;
self->temp_target = vec3_mulf(vec3_add(landing->center, landing->next->center), 0.5);
self->temp_target.y -= 2000;
self->velocity = vec3(0, 0, 0);
}
float brake = (self->brake_left + self->brake_right);
float resistance = (self->resistance * (SHIP_MAX_RESISTANCE - (brake * 0.125))) * 0.0078125;
vec3_t force = vec3(0, SHIP_FLYING_GRAVITY, 0);
force = vec3_add(force, self->thrust);
self->acceleration = vec3_divf(vec3_sub(forward_velocity, self->velocity), SHIP_MIN_RESISTANCE + brake * 4);
self->acceleration = vec3_add(self->acceleration, vec3_divf(force, self->mass));
self->acceleration = vec3_sub(self->acceleration, vec3_divf(self->velocity, resistance));
self->angular_acceleration.x += NTSC_ACCELERATION(ANGLE_NORM_TO_RADIAN(FIXED_TO_FLOAT(-50.0/16.0)));
}
// Position
self->velocity = vec3_add(self->velocity, vec3_mulf(self->acceleration, 30 * system_tick()));
self->position = vec3_add(self->position, vec3_mulf(self->velocity, 0.015625 * 30 * system_tick()));
self->angular_acceleration.x -= self->angular_velocity.x * 0.25 * 30;
self->angular_acceleration.z += (self->angular_velocity.y - 0.5 * self->angular_velocity.z) * 30;
// Orientation
if (self->angular_acceleration.y == 0) {
if (self->angular_velocity.y > 0) {
self->angular_acceleration.y -= min(self->turn_rate, self->angular_velocity.y / system_tick());
}
else if (self->angular_velocity.y < 0) {
self->angular_acceleration.y += min(self->turn_rate, -self->angular_velocity.y / system_tick());
}
}
self->angular_velocity = vec3_add(self->angular_velocity, vec3_mulf(self->angular_acceleration, system_tick()));
self->angular_velocity.y = clamp(self->angular_velocity.y, -self->turn_rate_max, self->turn_rate_max);
float brake_dir = (self->brake_left - self->brake_right) * (0.125 / 4096.0);
self->angle.y += brake_dir * self->speed * 0.000030517578125 * M_PI * 2 * 30 * system_tick();
self->angle = vec3_add(self->angle, vec3_mulf(self->angular_velocity, system_tick()));
self->angle.z -= self->angle.z * 0.125 * 30 * system_tick();
self->angle = vec3_wrap_angle(self->angle);
// Prevent ship from going past the landing position of a SECTION_JUMP if going backwards.
if (flags_not(self->flags, SHIP_DIRECTION_FORWARD) && flags_is(self->section->prev->flags, SECTION_JUMP)) {
vec3_t repulse = vec3_sub(self->section->next->center, self->section->center);
self->velocity = vec3_add(self->velocity, vec3_mulf(repulse, 2));
}
ship_player_update_sfx(self);
}
void ship_player_update_rescue(ship_t *self) {
section_t *next = self->section->next;
if (flags_is(self->flags, SHIP_IN_TOW)) {
self->temp_target = vec3_add(self->temp_target, vec3_mulf(vec3_sub(next->center, self->temp_target), 0.0078125));
self->velocity = vec3_sub(self->temp_target, self->position);
vec3_t target_dir = vec3_sub(next->center, self->section->center);
self->angular_velocity.y = wrap_angle(-atan2(target_dir.x, target_dir.z) - self->angle.y) * (1.0/16.0) * 30;
self->angle.y = wrap_angle(self->angle.y + self->angular_velocity.y * system_tick());
}
self->angle.x -= self->angle.x * 0.125 * 30 * system_tick();
self->angle.z -= self->angle.z * 0.03125 * 30 * system_tick();
self->velocity = vec3_sub(self->velocity, vec3_mulf(self->velocity, 0.0625 * 30 * system_tick()));
self->position = vec3_add(self->position, vec3_mulf(self->velocity, 0.03125 * 30 * system_tick()));
// Are we done being rescued?
float distance = vec3_len(vec3_sub(self->position, self->temp_target));
if (flags_is(self->flags, SHIP_IN_TOW) && distance < 800) {
self->update_func = ship_player_update_race;
self->update_timer = 0;
flags_rm(self->flags, SHIP_IN_RESCUE);
flags_rm(self->flags, SHIP_VIEW_REMOTE);
if (flags_is(self->flags, SHIP_VIEW_INTERNAL)) {
g.camera.update_func = camera_update_race_internal;
}
else {
g.camera.update_func = camera_update_race_external;
}
}
}
ship_t *ship_player_find_target(ship_t *self) {
int shortest_distance = 256;
ship_t *nearest_ship = NULL;
for (int i = 0; i < len(g.ships); i++) {
ship_t *other = &g.ships[i];
if (self == other) {
continue;
}
// We are on a branch
if (flags_is(self->section->flags, SECTION_JUNCTION)) {
// Other ship is on same branch
if (other->section->flags & SECTION_JUNCTION) {
int distance = other->section->num - self->section->num;
if (distance < shortest_distance && distance > 0) {
shortest_distance = distance;
nearest_ship = other;
}
}
// Other ship is not on branch
else {
section_t *section = self->section;
for (int distance = 0; distance < 10; distance++) {
section = section->next;
if (other->section == section && distance < shortest_distance && distance > 0) {
shortest_distance = distance;
nearest_ship = other;
break;
}
}
}
}
// We are not on a branch
else {
// Other ship is on a branch - check if we can reach the other ship's section
if (flags_is(other->section->flags, SECTION_JUNCTION)) {
section_t *section = self->section;
for (int distance = 0; distance < 10; distance++) {
if (section->junction) {
section = section->junction;
}
else {
section = section->next;
}
if (other->section == section && distance < shortest_distance && distance > 0) {
shortest_distance = distance;
nearest_ship = other;
break;
}
}
}
// Other ship is not on a branch
else {
int distance = other->section->num - self->section->num;
if (distance < shortest_distance && distance > 0) {
shortest_distance = distance;
nearest_ship = other;
}
}
}
}
if (shortest_distance < 10) {
return nearest_ship;
}
else {
return NULL;
}
}

@ -0,0 +1,20 @@
#ifndef SHIP_PLAYER_H
#define SHIP_PLAYER_H
#include "ship.h"
#define UPDATE_TIME_RESCUE (500.0 * (1.0/30.0))
#define UPDATE_TIME_STALL (90.0 * (1.0/30.0))
void ship_player_update_intro(ship_t *self);
void ship_player_update_intro_await_three(ship_t *self);
void ship_player_update_intro_await_two(ship_t *self);
void ship_player_update_intro_await_one(ship_t *self);
void ship_player_update_intro_await_go(ship_t *self);
void ship_player_update_intro_general(ship_t *self);
void ship_player_update_race(ship_t *self);
void ship_player_update_rescue(ship_t *self);
ship_t *ship_player_find_target(ship_t *self);
#endif

@ -0,0 +1,45 @@
#include "../system.h"
#include "../input.h"
#include "../utils.h"
#include "title.h"
#include "ui.h"
#include "image.h"
#include "game.h"
static uint16_t title_image;
static float start_time;
static bool has_shown_attract = false;
void title_init() {
title_image = image_get_texture("wipeout/textures/wiptitle.tim");
start_time = system_time();
sfx_music_mode(SFX_MUSIC_RANDOM);
}
void title_update() {
render_set_view_2d();
render_push_2d(vec2i(0, 0), render_size(), rgba(128, 128, 128, 255), title_image);
ui_draw_text_centered("PRESS ENTER", ui_scaled_pos(UI_POS_BOTTOM | UI_POS_CENTER, vec2i(0, -40)), UI_SIZE_8, UI_COLOR_DEFAULT);
if (input_pressed(A_MENU_SELECT) || input_pressed(A_MENU_START)) {
sfx_play(SFX_MENU_SELECT);
game_set_scene(GAME_SCENE_MAIN_MENU);
}
float duration = system_time() - start_time;
if (
(has_shown_attract && duration > 5) ||
(duration > 10)
) {
sfx_music_mode(SFX_MUSIC_RANDOM);
has_shown_attract = true;
g.is_attract_mode = true;
g.pilot = rand_int(0, len(def.pilots));
g.circut = rand_int(0, NUM_NON_BONUS_CIRCUTS);
g.race_class = rand_int(0, NUM_RACE_CLASSES);
g.race_type = RACE_TYPE_SINGLE;
game_set_scene(GAME_SCENE_RACE);
}
}

@ -0,0 +1,8 @@
#ifndef TITLE_H
#define TITLE_H
void title_init();
void title_update();
void title_cleanup();
#endif

@ -0,0 +1,387 @@
#include "../mem.h"
#include "../utils.h"
#include "../render.h"
#include "../system.h"
#include "object.h"
#include "track.h"
#include "camera.h"
#include "object.h"
#include "game.h"
void track_load(const char *base_path) {
// Load and assemble high res track tiles
g.track.textures.start = render_textures_len();
g.track.textures.len = 0;
ttf_t *ttf = track_load_tile_format(get_path(base_path, "library.ttf"));
cmp_t *cmp = image_load_compressed(get_path(base_path, "library.cmp"));
image_t *temp_tile = image_alloc(128, 128);
for (int i = 0; i < ttf->len; i++) {
for (int tx = 0; tx < 4; tx++) {
for (int ty = 0; ty < 4; ty++) {
uint32_t sub_tile_index = ttf->tiles[i].near[ty * 4 + tx];
image_t *sub_tile = image_load_from_bytes(cmp->entries[sub_tile_index], false);
image_copy(sub_tile, temp_tile, 0, 0, 32, 32, tx * 32, ty * 32);
mem_temp_free(sub_tile);
}
}
render_texture_create(temp_tile->width, temp_tile->height, temp_tile->pixels);
g.track.textures.len++;
}
mem_temp_free(temp_tile);
mem_temp_free(cmp);
mem_temp_free(ttf);
vec3_t *vertices = track_load_vertices(get_path(base_path, "track.trv"));
track_load_faces(get_path(base_path, "track.trf"), vertices);
mem_temp_free(vertices);
track_load_sections(get_path(base_path, "track.trs"));
g.track.pickups_len = 0;
section_t *s = g.track.sections;
section_t *j = NULL;
// Nummerate all sections; take care to give both stretches at a junction
// the same numbers.
int num = 0;
do {
s->num = num++;
if (s->junction) { // start junction
j = s->junction;
do {
j->num = num++;
j = j->next;
} while (!j->junction); // end junction
num = s->num;
}
s = s->next;
} while (s != g.track.sections);
g.track.total_section_nums = num;
g.track.pickups = mem_mark();
for (int i = 0; i < g.track.section_count; i++) {
track_face_t *face = track_section_get_base_face(&g.track.sections[i]);
for (int f = 0; f < 2; f++) {
if (flags_any(face->flags, FACE_PICKUP_RIGHT | FACE_PICKUP_LEFT)) {
mem_bump(sizeof(track_pickup_t));
g.track.pickups[g.track.pickups_len].face = face;
g.track.pickups[g.track.pickups_len].cooldown_timer = 0;
g.track.pickups_len++;
}
if (flags_is(face->flags, FACE_BOOST)) {
track_face_set_color(face, rgba(0, 0, 255, 255));
}
face++;
}
error_if(g.track.pickups_len > TRACK_PICKUPS_MAX-1, "Track %s exceeds TRACK_PICKUPS_MAX", base_path);
}
}
ttf_t *track_load_tile_format(char *ttf_name) {
uint32_t ttf_size;
uint8_t *ttf_bytes = file_load(ttf_name, &ttf_size);
uint32_t p = 0;
uint32_t num_tiles = ttf_size / 42;
ttf_t *ttf = mem_temp_alloc(sizeof(ttf_t) + sizeof(ttf_tile_t) * num_tiles);
ttf->len = num_tiles;
for (int t = 0; t < num_tiles; t++) {
for (int i = 0; i < 16; i++) {
ttf->tiles[t].near[i] = get_i16(ttf_bytes, &p);
}
for (int i = 0; i < 4; i++) {
ttf->tiles[t].med[i] = get_i16(ttf_bytes, &p);
}
ttf->tiles[t].far = get_i16(ttf_bytes, &p);
}
mem_temp_free(ttf_bytes);
return ttf;
}
bool track_collect_pickups(track_face_t *face) {
if (flags_is(face->flags, FACE_PICKUP_ACTIVE)) {
flags_rm(face->flags, FACE_PICKUP_ACTIVE);
flags_add(face->flags, FACE_PICKUP_COLLECTED);
track_face_set_color(face, rgba(255, 255, 255, 255));
return true;
}
else {
return false;
}
}
vec3_t *track_load_vertices(char *file_name) {
uint32_t size;
uint8_t *bytes = file_load(file_name, &size);
g.track.vertex_count = size / 16; // VECTOR_SIZE
vec3_t *vertices = mem_temp_alloc(sizeof(vec3_t) * g.track.vertex_count);
uint32_t p = 0;
for (int i = 0; i < g.track.vertex_count; i++) {
vertices[i].x = get_i32(bytes, &p);
vertices[i].y = get_i32(bytes, &p);
vertices[i].z = get_i32(bytes, &p);
p += 4; // padding
}
mem_temp_free(bytes);
return vertices;
}
static const vec2_t track_uv[2][4] = {
{{128, 0}, { 0, 0}, { 0, 128}, {128, 128}},
{{ 0, 0}, {128, 0}, {128, 128}, { 0, 128}}
};
void track_load_faces(char *file_name, vec3_t *vertices) {
uint32_t size;
uint8_t *bytes = file_load(file_name, &size);
g.track.face_count = size / 20; // TRACK_FACE_DATA_SIZE
g.track.faces = mem_bump(sizeof(track_face_t) * g.track.face_count);
uint32_t p = 0;
track_face_t *tf = g.track.faces;
for (int i = 0; i < g.track.face_count; i++) {
vec3_t v0 = vertices[get_i16(bytes, &p)];
vec3_t v1 = vertices[get_i16(bytes, &p)];
vec3_t v2 = vertices[get_i16(bytes, &p)];
vec3_t v3 = vertices[get_i16(bytes, &p)];
tf->normal.x = (float)get_i16(bytes, &p) / 4096.0;
tf->normal.y = (float)get_i16(bytes, &p) / 4096.0;
tf->normal.z = (float)get_i16(bytes, &p) / 4096.0;
tf->texture = get_i8(bytes, &p);
tf->flags = get_i8(bytes, &p);
rgba_t color = {.as_uint32 = get_i32_le(bytes, &p) | 0xff000000};
const vec2_t *uv = track_uv[flags_is(tf->flags, FACE_FLIP_TEXTURE) ? 1 : 0];
tf->tris[0] = (tris_t){
.vertices = {
{.pos = v0, .uv = uv[0], .color = color},
{.pos = v1, .uv = uv[1], .color = color},
{.pos = v2, .uv = uv[2], .color = color},
}
};
tf->tris[1] = (tris_t){
.vertices = {
{.pos = v3, .uv = uv[3], .color = color},
{.pos = v0, .uv = uv[0], .color = color},
{.pos = v2, .uv = uv[2], .color = color},
}
};
tf++;
}
mem_temp_free(bytes);
}
void track_load_sections(char *file_name) {
uint32_t size;
uint8_t *bytes = file_load(file_name, &size);
g.track.section_count = size / 156; // SECTION_DATA_SIZE
g.track.sections = mem_bump(sizeof(section_t) * g.track.section_count);
uint32_t p = 0;
section_t *ts = g.track.sections;
for (int i = 0; i < g.track.section_count; i++) {
int32_t junction_index = get_i32(bytes, &p);
if (junction_index != -1) {
ts->junction = g.track.sections + junction_index;
}
else {
ts->junction = NULL;
}
ts->prev = g.track.sections + get_i32(bytes, &p);
ts->next = g.track.sections + get_i32(bytes, &p);
ts->center.x = get_i32(bytes, &p);
ts->center.y = get_i32(bytes, &p);
ts->center.z = get_i32(bytes, &p);
int16_t version = get_i16(bytes, &p);
error_if(version != TRACK_VERSION, "Convert track with track10: section: %d Track: %d\n", version, TRACK_VERSION);
p += 2; // padding
p += 4 + 4; // objects pointer, objectCount
p += 5 * 3 * 4; // view section pointers
p += 5 * 3 * 2; // view section counts
for (int j = 0; j < 4; j++) {
ts->high[j] = get_i16(bytes, &p);
}
for (int j = 0; j < 4; j++) {
ts->med[j] = get_i16(bytes, &p);
}
ts->face_start = get_i16(bytes, &p);
ts->face_count = get_i16(bytes, &p);
p += 2 * 2; // global/local radius
ts->flags = get_i16(bytes, &p);
ts->num = get_i16(bytes, &p);
p += 2; // padding
ts++;
}
mem_temp_free(bytes);
}
void track_draw_section(section_t *section) {
track_face_t *face = g.track.faces + section->face_start;
int16_t face_count = section->face_count;
for (uint32_t j = 0; j < face_count; j++) {
uint16_t tex_index = texture_from_list(g.track.textures, face->texture);
render_push_tris(face->tris[0], tex_index);
render_push_tris(face->tris[1], tex_index);
face++;
}
}
void track_draw(camera_t *camera) {
render_set_model_mat(&mat4_identity());
float max_dist_sq = RENDER_FADEOUT_FAR * RENDER_FADEOUT_FAR;
vec3_t cam_pos = camera->position;
section_t *s = g.track.sections;
section_t *j = NULL;
do {
vec3_t d = vec3_sub(cam_pos, s->center);
float dist_sq = d.x * d.x + d.y * d.y + d.z * d.z;
if (dist_sq < max_dist_sq) {
track_draw_section(s);
}
if (s->junction) { // start junction
j = s->junction;
do {
vec3_t d = vec3_sub(cam_pos, j->center);
float dist_sq = d.x * d.x + d.y * d.y + d.z * d.z;
if (dist_sq < max_dist_sq) {
track_draw_section(j);
}
j = j->next;
} while (!j->junction); // end junction
}
s = s->next;
} while (s != g.track.sections);
}
void track_cycle_pickups() {
float pickup_cycle_time = 1.5 * system_cycle_time();
for (int i = 0; i < g.track.pickups_len; i++) {
if (flags_is(g.track.pickups[i].face->flags, FACE_PICKUP_COLLECTED)) {
flags_rm(g.track.pickups[i].face->flags, FACE_PICKUP_COLLECTED);
g.track.pickups[i].cooldown_timer = TRACK_PICKUP_COOLDOWN_TIME;
}
else if (g.track.pickups[i].cooldown_timer <= 0) {
flags_add(g.track.pickups[i].face->flags, FACE_PICKUP_ACTIVE);
track_face_set_color(g.track.pickups[i].face, rgba(
sin( pickup_cycle_time + i) * 127 + 128,
cos( pickup_cycle_time + i) * 127 + 128,
sin(-pickup_cycle_time - i) * 127 + 128,
255
));
}
else{
g.track.pickups[i].cooldown_timer -= system_tick();
}
}
}
void track_face_set_color(track_face_t *face, rgba_t color) {
face->tris[0].vertices[0].color = color;
face->tris[0].vertices[1].color = color;
face->tris[0].vertices[2].color = color;
face->tris[1].vertices[0].color = color;
face->tris[1].vertices[1].color = color;
face->tris[1].vertices[2].color = color;
}
track_face_t *track_section_get_base_face(section_t *section) {
track_face_t *face = g.track.faces +section->face_start;
while(flags_not(face->flags, FACE_TRACK_BASE)) {
face++;
}
return face;
}
section_t *track_nearest_section(vec3_t pos, section_t *section, float *distance) {
// Start search several sections before current section
for (int i = 0; i < TRACK_SEARCH_LOOK_BACK; i++) {
section = section->prev;
}
// Find vector from ship center to track section under
// consideration
float shortest_distance = 1000000000.0;
section_t *nearest_section = section;
section_t *junction = NULL;
for (int i = 0; i < TRACK_SEARCH_LOOK_AHEAD; i++) {
if (section->junction) {
junction = section->junction;
}
float d = vec3_len(vec3_sub(pos, section->center));
if (d < shortest_distance) {
shortest_distance = d;
nearest_section = section;
}
section = section->next;
}
if (junction) {
section = junction;
for (int i = 0; i < TRACK_SEARCH_LOOK_AHEAD; i++) {
float d = vec3_len(vec3_sub(pos, section->center));
if (d < shortest_distance) {
shortest_distance = d;
nearest_section = section;
}
if (flags_is(junction->flags, SECTION_JUNCTION_START)) {
section = section->next;
}
else {
section = section->prev;
}
}
}
if (distance != NULL) {
*distance = shortest_distance;
}
return nearest_section;
}

@ -0,0 +1,104 @@
#ifndef TRACK_H
#define TRACK_H
#include "../types.h"
#include "object.h"
#include "image.h"
#define TRACK_VERSION 8
#define TRACK_VERTS_MAX 4096
#define TRACK_FACES_MAX 3072
#define TRACK_SECTIONS_MAX 1024
#define TRACK_PICKUPS_MAX 64
#define TRACK_PICKUP_COOLDOWN_TIME 1
#define TRACK_SEARCH_LOOK_BACK 3
#define TRACK_SEARCH_LOOK_AHEAD 6
typedef struct track_face_t {
tris_t tris[2];
vec3_t normal;
uint8_t flags;
uint8_t texture;
} track_face_t;
#define FACE_TRACK_BASE (1<<0)
#define FACE_PICKUP_LEFT (1<<1)
#define FACE_FLIP_TEXTURE (1<<2)
#define FACE_PICKUP_RIGHT (1<<3)
#define FACE_START_GRID (1<<4)
#define FACE_BOOST (1<<5)
#define FACE_PICKUP_COLLECTED (1<<6)
#define FACE_PICKUP_ACTIVE (1<<7)
typedef struct {
uint16_t near[16];
uint16_t med[4];
uint16_t far;
} ttf_tile_t;
typedef struct {
uint32_t len;
ttf_tile_t tiles[];
} ttf_t;
typedef struct section_t {
struct section_t *junction;
struct section_t *prev;
struct section_t *next;
vec3_t center;
int16_t high[4];
int16_t med[4];
int16_t face_start;
int16_t face_count;
int16_t flags;
int16_t num;
} section_t;
#define SECTION_JUMP 1
#define SECTION_JUNCTION_END 8
#define SECTION_JUNCTION_START 16
#define SECTION_JUNCTION 32
typedef struct {
track_face_t *face;
float cooldown_timer;
} track_pickup_t;
typedef struct track_t {
int32_t vertex_count;
int32_t face_count;
int32_t section_count;
int32_t pickups_len;
int32_t total_section_nums;
texture_list_t textures;
track_face_t *faces;
section_t *sections;
track_pickup_t *pickups;
} track_t;
void track_load(const char *base_path);
ttf_t *track_load_tile_format(char *ttf_name);
vec3_t *track_load_vertices(char *file);
void track_load_faces(char *file, vec3_t *vertices);
void track_load_sections(char *file);
bool track_collect_pickups(track_face_t *face);
void track_face_set_color(track_face_t *face, rgba_t color);
track_face_t *track_section_get_base_face(section_t *section);
section_t *track_nearest_section(vec3_t pos, section_t *section, float *distance);
struct camera_t;
void track_draw(struct camera_t *camera);
void track_cycle_pickups();
#endif

@ -0,0 +1,213 @@
#include "../render.h"
#include "../utils.h"
#include "ui.h"
#include "image.h"
typedef struct {
vec2i_t offset;
uint16_t width;
} glyph_t;
typedef struct {
uint16_t texture;
uint16_t height;
glyph_t glyphs[40];
} char_set_t;
int ui_scale = 2;
char_set_t char_set[UI_SIZE_MAX] = {
[UI_SIZE_16] = {
.texture = 0,
.height = 16,
.glyphs = {
{{ 0, 0}, 25}, {{ 25, 0}, 24}, {{ 49, 0}, 17}, {{ 66, 0}, 24}, {{ 90, 0}, 24}, {{114, 0}, 17}, {{131, 0}, 25}, {{156, 0}, 18},
{{174, 0}, 7}, {{181, 0}, 17}, {{ 0, 16}, 17}, {{ 17, 16}, 17}, {{ 34, 16}, 28}, {{ 62, 16}, 17}, {{ 79, 16}, 24}, {{103, 16}, 24},
{{127, 16}, 26}, {{153, 16}, 24}, {{177, 16}, 18}, {{195, 16}, 17}, {{ 0, 32}, 17}, {{ 17, 32}, 17}, {{ 34, 32}, 29}, {{ 63, 32}, 24},
{{ 87, 32}, 17}, {{104, 32}, 18}, {{122, 32}, 24}, {{146, 32}, 10}, {{156, 32}, 18}, {{174, 32}, 17}, {{191, 32}, 18}, {{ 0, 48}, 18},
{{ 18, 48}, 18}, {{ 36, 48}, 18}, {{ 54, 48}, 22}, {{ 76, 48}, 25}, {{101, 48}, 7}, {{108, 48}, 7}, {{198, 0}, 0}, {{198, 0}, 0}
}
},
[UI_SIZE_12] = {
.texture = 0,
.height = 12,
.glyphs = {
{{ 0, 0}, 19}, {{ 19, 0}, 19}, {{ 38, 0}, 14}, {{ 52, 0}, 19}, {{ 71, 0}, 19}, {{ 90, 0}, 13}, {{103, 0}, 19}, {{122, 0}, 14},
{{136, 0}, 6}, {{142, 0}, 13}, {{155, 0}, 14}, {{169, 0}, 14}, {{ 0, 12}, 22}, {{ 22, 12}, 14}, {{ 36, 12}, 19}, {{ 55, 12}, 18},
{{ 73, 12}, 20}, {{ 93, 12}, 19}, {{112, 12}, 15}, {{127, 12}, 14}, {{141, 12}, 13}, {{154, 12}, 13}, {{167, 12}, 22}, {{ 0, 24}, 19},
{{ 19, 24}, 13}, {{ 32, 24}, 14}, {{ 46, 24}, 19}, {{ 65, 24}, 8}, {{ 73, 24}, 15}, {{ 88, 24}, 13}, {{101, 24}, 14}, {{115, 24}, 15},
{{130, 24}, 14}, {{144, 24}, 15}, {{159, 24}, 18}, {{177, 24}, 19}, {{196, 24}, 5}, {{201, 24}, 5}, {{183, 0}, 0}, {{183, 0}, 0}
}
},
[UI_SIZE_8] = {
.texture = 0,
.height = 8,
.glyphs = {
{{ 0, 0}, 13}, {{ 13, 0}, 13}, {{ 26, 0}, 10}, {{ 36, 0}, 13}, {{ 49, 0}, 13}, {{ 62, 0}, 9}, {{ 71, 0}, 13}, {{ 84, 0}, 10},
{{ 94, 0}, 4}, {{ 98, 0}, 9}, {{107, 0}, 10}, {{117, 0}, 10}, {{127, 0}, 16}, {{143, 0}, 10}, {{153, 0}, 13}, {{166, 0}, 13},
{{179, 0}, 14}, {{ 0, 8}, 13}, {{ 13, 8}, 10}, {{ 23, 8}, 9}, {{ 32, 8}, 9}, {{ 41, 8}, 9}, {{ 50, 8}, 16}, {{ 66, 8}, 14},
{{ 80, 8}, 9}, {{ 89, 8}, 10}, {{ 99, 8}, 13}, {{112, 8}, 6}, {{118, 8}, 11}, {{129, 8}, 10}, {{139, 8}, 10}, {{149, 8}, 11},
{{160, 8}, 10}, {{170, 8}, 10}, {{180, 8}, 12}, {{192, 8}, 14}, {{206, 8}, 4}, {{210, 8}, 4}, {{193, 0}, 0}, {{193, 0}, 0}
}
},
};
uint16_t icon_textures[UI_ICON_MAX];
void ui_load() {
texture_list_t tl = image_get_compressed_textures("wipeout/textures/drfonts.cmp");
char_set[UI_SIZE_16].texture = texture_from_list(tl, 0);
char_set[UI_SIZE_12].texture = texture_from_list(tl, 1);
char_set[UI_SIZE_8 ].texture = texture_from_list(tl, 2);
icon_textures[UI_ICON_HAND] = texture_from_list(tl, 3);
icon_textures[UI_ICON_CONFIRM] = texture_from_list(tl, 5);
icon_textures[UI_ICON_CANCEL] = texture_from_list(tl, 6);
icon_textures[UI_ICON_END] = texture_from_list(tl, 7);
icon_textures[UI_ICON_DEL] = texture_from_list(tl, 8);
icon_textures[UI_ICON_STAR] = texture_from_list(tl, 9);
}
int ui_get_scale() {
return ui_scale;
}
void ui_set_scale(int scale) {
ui_scale = scale;
}
vec2i_t ui_scaled(vec2i_t v) {
return vec2i(v.x * ui_scale, v.y * ui_scale);
}
vec2i_t ui_scaled_screen() {
return vec2i_mulf(render_size(), ui_scale);
}
vec2i_t ui_scaled_pos(ui_pos_t anchor, vec2i_t offset) {
vec2i_t pos;
vec2i_t screen_size = render_size();
if (flags_is(anchor, UI_POS_LEFT)) {
pos.x = offset.x * ui_scale;
}
else if (flags_is(anchor, UI_POS_CENTER)) {
pos.x = (screen_size.x >> 1) + offset.x * ui_scale;
}
else if (flags_is(anchor, UI_POS_RIGHT)) {
pos.x = screen_size.x + offset.x * ui_scale;
}
if (flags_is(anchor, UI_POS_TOP)) {
pos.y = offset.y * ui_scale;
}
else if (flags_is(anchor, UI_POS_MIDDLE)) {
pos.y = (screen_size.y >> 1) + offset.y * ui_scale;
}
else if (flags_is(anchor, UI_POS_BOTTOM)) {
pos.y = screen_size.y + offset.y * ui_scale;
}
return pos;
}
#define char_to_glyph_index(C) (C >= '0' && C <= '9' ? (C - '0' + 26) : C - 'A')
int ui_char_width(char c, ui_text_size_t size) {
if (c == ' ') {
return 8;
}
return char_set[size].glyphs[char_to_glyph_index(c)].width;
}
int ui_text_width(const char *text, ui_text_size_t size) {
int width = 0;
char_set_t *cs = &char_set[size];
for (int i = 0; text[i] != 0; i++) {
width += text[i] != ' '
? cs->glyphs[char_to_glyph_index(text[i])].width
: 8;
}
return width;
}
int ui_number_width(int num, ui_text_size_t size) {
char text_buffer[16];
text_buffer[15] = '\0';
int i;
for (i = 14; i > 0; i--) {
text_buffer[i] = '0' + (num % 10);
num = num / 10;
if (num == 0) {
break;
}
}
return ui_text_width(text_buffer + i, size);
}
void ui_draw_time(float time, vec2i_t pos, ui_text_size_t size, rgba_t color) {
int msec = time * 1000;
int tenths = (msec / 100) % 10;
int secs = (msec / 1000) % 60;
int mins = msec / (60 * 1000);
char text_buffer[8];
text_buffer[0] = '0' + (mins / 10) % 10;
text_buffer[1] = '0' + mins % 10;
text_buffer[2] = 'e'; // ":"
text_buffer[3] = '0' + secs / 10;
text_buffer[4] = '0' + secs % 10;
text_buffer[5] = 'f'; // "."
text_buffer[6] = '0' + tenths;
text_buffer[7] = '\0';
ui_draw_text(text_buffer, pos, size, color);
}
void ui_draw_number(int num, vec2i_t pos, ui_text_size_t size, rgba_t color) {
char text_buffer[16];
text_buffer[15] = '\0';
int i;
for (i = 14; i > 0; i--) {
text_buffer[i] = '0' + (num % 10);
num = num / 10;
if (num == 0) {
break;
}
}
ui_draw_text(text_buffer + i, pos, size, color);
}
void ui_draw_text(const char *text, vec2i_t pos, ui_text_size_t size, rgba_t color) {
char_set_t *cs = &char_set[size];
for (int i = 0; text[i] != 0; i++) {
if (text[i] != ' ') {
glyph_t *glyph = &cs->glyphs[char_to_glyph_index(text[i])];
vec2i_t size = vec2i(glyph->width, cs->height);
render_push_2d_tile(pos, glyph->offset, size, ui_scaled(size), color, cs->texture);
pos.x += glyph->width * ui_scale;
}
else {
pos.x += 8 * ui_scale;
}
}
}
void ui_draw_image(vec2i_t pos, uint16_t texture) {
vec2i_t scaled_size = ui_scaled(render_texture_size(texture));
render_push_2d(pos, scaled_size, rgba(128, 128, 128, 255), texture);
}
void ui_draw_icon(ui_icon_type_t icon, vec2i_t pos, rgba_t color) {
render_push_2d(pos, ui_scaled(render_texture_size(icon_textures[icon])), color, icon_textures[icon]);
}
void ui_draw_text_centered(const char *text, vec2i_t pos, ui_text_size_t size, rgba_t color) {
pos.x -= (ui_text_width(text, size) * ui_scale) >> 1;
ui_draw_text(text, pos, size, color);
}

@ -0,0 +1,56 @@
#ifndef UI_H
#define UI_H
#include "../types.h"
typedef enum {
UI_SIZE_16,
UI_SIZE_12,
UI_SIZE_8,
UI_SIZE_MAX
} ui_text_size_t;
#define UI_COLOR_ACCENT rgba(123, 98, 12, 255)
#define UI_COLOR_DEFAULT rgba(128, 128, 128, 255)
typedef enum {
UI_ICON_HAND,
UI_ICON_CONFIRM,
UI_ICON_CANCEL,
UI_ICON_END,
UI_ICON_DEL,
UI_ICON_STAR,
UI_ICON_MAX
} ui_icon_type_t;
typedef enum {
UI_POS_LEFT = 1 << 0,
UI_POS_CENTER = 1 << 1,
UI_POS_RIGHT = 1 << 2,
UI_POS_TOP = 1 << 3,
UI_POS_MIDDLE = 1 << 4,
UI_POS_BOTTOM = 1 << 5,
} ui_pos_t;
void ui_load();
void ui_cleanup();
int ui_get_scale();
void ui_set_scale(int scale);
vec2i_t ui_scaled(vec2i_t v);
vec2i_t ui_scaled_screen();
vec2i_t ui_scaled_pos(ui_pos_t anchor, vec2i_t offset);
int ui_char_width(char c, ui_text_size_t size);
int ui_text_width(const char *text, ui_text_size_t size);
int ui_number_width(int num, ui_text_size_t size);
void ui_draw_text(const char *text, vec2i_t pos, ui_text_size_t size, rgba_t color);
void ui_draw_time(float time, vec2i_t pos, ui_text_size_t size, rgba_t color);
void ui_draw_number(int num, vec2i_t pos, ui_text_size_t size, rgba_t color);
void ui_draw_image(vec2i_t pos, uint16_t texture);
void ui_draw_icon(ui_icon_type_t icon, vec2i_t pos, rgba_t color);
void ui_draw_text_centered(const char *text, vec2i_t pos, ui_text_size_t size, rgba_t color);
#endif

@ -0,0 +1,689 @@
#include "../mem.h"
#include "../utils.h"
#include "../system.h"
#include "track.h"
#include "ship.h"
#include "weapon.h"
#include "object.h"
#include "game.h"
#include "image.h"
#include "particle.h"
extern int32_t ctrlNeedTargetIcon;
extern int ctrlnearShip;
int16_t Shielded = 0;
typedef struct weapon_t {
float timer;
ship_t *owner;
ship_t *target;
section_t *section;
Object *model;
bool active;
int16_t trail_particle;
int16_t track_hit_particle;
int16_t ship_hit_particle;
float trail_spawn_timer;
int16_t type;
vec3_t acceleration;
vec3_t velocity;
vec3_t position;
vec3_t angle;
float drag;
void (*update_func)(struct weapon_t *);
} weapon_t;
weapon_t *weapons;
int weapons_active = 0;
struct {
uint16_t reticle;
Object *rocket;
Object *mine;
Object *missile;
Object *shield;
Object *shield_internal;
Object *ebolt;
} weapon_assets;
void weapon_update_wait_for_delay(weapon_t *self);
void weapon_fire_mine(ship_t *ship);
void weapon_update_mine_wait_for_release(weapon_t *self);
void weapon_update_mine(weapon_t *self);
void weapon_update_mine_lights(weapon_t *self, int index);
void weapon_fire_missile(ship_t *ship);
void weapon_update_missile(weapon_t *self);
void weapon_fire_rocket(ship_t *ship);
void weapon_update_rocket(weapon_t *self);
void weapon_fire_ebolt(ship_t *ship);
void weapon_update_ebolt(weapon_t *self);
void weapon_fire_shield(ship_t *ship);
void weapon_update_shield(weapon_t *self);
void weapon_fire_turbo(ship_t *ship);
void invert_shield_polys(Object *shield);
void weapons_load() {
weapons = mem_bump(sizeof(weapon_t) * WEAPONS_MAX);
weapon_assets.reticle = image_get_texture("wipeout/textures/target2.tim");
texture_list_t weapon_textures = image_get_compressed_textures("wipeout/common/mine.cmp");
weapon_assets.rocket = objects_load("wipeout/common/rock.prm", weapon_textures);
weapon_assets.mine = objects_load("wipeout/common/mine.prm", weapon_textures);
weapon_assets.missile = objects_load("wipeout/common/miss.prm", weapon_textures);
weapon_assets.shield = objects_load("wipeout/common/shld.prm", weapon_textures);
weapon_assets.shield_internal = objects_load("wipeout/common/shld.prm", weapon_textures);
weapon_assets.ebolt = objects_load("wipeout/common/ebolt.prm", weapon_textures);
// Invert shield polys for internal view
Prm poly = {.primitive = weapon_assets.shield_internal->primitives};
int primitives_len = weapon_assets.shield_internal->primitives_len;
for (int k = 0; k < primitives_len; k++) {
switch (poly.primitive->type) {
case PRM_TYPE_G3 :
swap(poly.g3->coords[0], poly.g3->coords[2]);
poly.g3 += 1;
break;
case PRM_TYPE_G4 :
swap(poly.g4->coords[0], poly.g4->coords[3]);
poly.g4 += 1;
break;
}
}
weapons_init();
}
void weapons_init() {
weapons_active = 0;
}
weapon_t *weapon_init(ship_t *ship) {
if (weapons_active == WEAPONS_MAX) {
return NULL;
}
weapon_t *weapon = &weapons[weapons_active++];
weapon->timer = 0;
weapon->owner = ship;
weapon->section = ship->section;
weapon->position = ship->position;
weapon->angle = ship->angle;
weapon->acceleration = vec3(0, 0, 0);
weapon->velocity = vec3(0, 0, 0);
weapon->acceleration = vec3(0, 0, 0);
weapon->target = NULL;
weapon->model = NULL;
weapon->active = true;
weapon->trail_particle = PARTICLE_TYPE_NONE;
weapon->track_hit_particle = PARTICLE_TYPE_NONE;
weapon->ship_hit_particle = PARTICLE_TYPE_NONE;
weapon->trail_spawn_timer = 0;
weapon->drag = 0;
return weapon;
}
void weapons_fire(ship_t *ship, int weapon_type) {
switch (weapon_type) {
case WEAPON_TYPE_MINE: weapon_fire_mine(ship); break;
case WEAPON_TYPE_MISSILE: weapon_fire_missile(ship); break;
case WEAPON_TYPE_ROCKET: weapon_fire_rocket(ship); break;
case WEAPON_TYPE_EBOLT: weapon_fire_ebolt(ship); break;
case WEAPON_TYPE_SHIELD: weapon_fire_shield(ship); break;
case WEAPON_TYPE_TURBO: weapon_fire_turbo(ship); break;
default: die("Inavlid weapon type %d", weapon_type);
}
ship->weapon_type = WEAPON_TYPE_NONE;
}
void weapons_fire_delayed(ship_t *ship, int weapon_type) {
weapon_t *weapon = weapon_init(ship);
if (!weapon) {
return;
}
weapon->type = weapon_type;
weapon->timer = WEAPON_AI_DELAY;
weapon->update_func = weapon_update_wait_for_delay;
}
bool weapon_collides_with_track(weapon_t *self);
void weapons_update() {
for (int i = 0; i < weapons_active; i++) {
weapon_t *weapon = &weapons[i];
weapon->timer -= system_tick();
(weapon->update_func)(weapon);
// Handle projectiles
if (weapon->acceleration.x != 0 || weapon->acceleration.z != 0) {
weapon->velocity = vec3_add(weapon->velocity, vec3_mulf(weapon->acceleration, 30 * system_tick()));
weapon->velocity = vec3_sub(weapon->velocity, vec3_mulf(weapon->velocity, weapon->drag * 30 * system_tick()));
weapon->position = vec3_add(weapon->position, vec3_mulf(weapon->velocity, 30 * system_tick()));
// Move along track normal
track_face_t *face = track_section_get_base_face(weapon->section);
vec3_t face_point = face->tris[0].vertices[0].pos;
vec3_t face_normal = face->normal;
float height = vec3_distance_to_plane(weapon->position, face_point, face_normal);
if (height < 2000) {
weapon->position = vec3_add(weapon->position, vec3_mulf(face_normal, (200 - height) * 30 * system_tick()));
}
// Trail
if (weapon->trail_particle != PARTICLE_TYPE_NONE) {
weapon->trail_spawn_timer += system_tick();
while (weapon->trail_spawn_timer > 0) {
vec3_t pos = vec3_sub(weapon->position, vec3_mulf(weapon->velocity, 30 * system_tick() * weapon->trail_spawn_timer));
vec3_t velocity = vec3(rand_float(-128, 128), rand_float(-128, 128), rand_float(-128, 128));
particles_spawn(pos, weapon->trail_particle, velocity, 128);
weapon->trail_spawn_timer -= WEAPON_PARTICLE_SPAWN_RATE;
}
}
// Track collision
weapon->section = track_nearest_section(weapon->position, weapon->section, NULL);
if (weapon_collides_with_track(weapon)) {
for (int p = 0; p < 32; p++) {
vec3_t velocity = vec3(rand_float(-512, 512), rand_float(-512, 512), rand_float(-512, 512));
particles_spawn(weapon->position, weapon->track_hit_particle, velocity, 256);
}
sfx_play_at(SFX_EXPLOSION_2, weapon->position, vec3(0,0,0), 1);
weapon->active = false;
}
}
// If this weapon is released, we have to rewind one step
if (!weapon->active) {
weapons[i--] = weapons[--weapons_active];
continue;
}
}
}
void weapons_draw() {
mat4_t mat = mat4_identity();
for (int i = 0; i < weapons_active; i++) {
weapon_t *weapon = &weapons[i];
if (weapon->model) {
mat4_set_translation(&mat, weapon->position);
mat4_set_yaw_pitch_roll(&mat, weapon->angle);
if (weapon->model == weapon_assets.mine) {
weapon_update_mine_lights(weapon, i);
}
object_draw(weapon->model, &mat);
}
}
}
void weapon_set_trajectory(weapon_t *self) {
ship_t *ship = self->owner;
track_face_t *face = track_section_get_base_face(ship->section);
vec3_t face_point = face->tris[0].vertices[0].pos;
vec3_t target = vec3_add(ship->position, vec3_mulf(ship->dir_forward, 64));
float target_height = vec3_distance_to_plane(target, face_point, face->normal);
float ship_height = vec3_distance_to_plane(target, face_point, face->normal);
float nudge = target_height * 0.95 - ship_height;
self->acceleration = vec3_sub(vec3_sub(target, vec3_mulf(face->normal, nudge)), ship->position);
self->velocity = vec3_mulf(ship->velocity, 0.015625);
self->angle = ship->angle;
}
void weapon_follow_target(weapon_t *self) {
vec3_t angular_velocity = vec3(0, 0, 0);
if (self->target) {
vec3_t dir = vec3_mulf(vec3_sub(self->target->position, self->position), 0.125 * 30 * system_tick());
float height = sqrt(dir.x * dir.x + dir.z * dir.z);
angular_velocity.y = -atan2(dir.x, dir.z) - self->angle.y;
angular_velocity.x = -atan2(dir.y, height) - self->angle.x;
}
angular_velocity = vec3_wrap_angle(angular_velocity);
self->angle = vec3_add(self->angle, vec3_mulf(angular_velocity, 30 * system_tick() * 0.25));
self->angle = vec3_wrap_angle(self->angle);
self->acceleration.x = -sin(self->angle.y) * cos(self->angle.x) * 256;
self->acceleration.y = -sin(self->angle.x) * 256;
self->acceleration.z = cos(self->angle.y) * cos(self->angle.x) * 256;
}
ship_t *weapon_collides_with_ship(weapon_t *self) {
for (int i = 0; i < NUM_PILOTS; i++) {
ship_t *ship = &g.ships[i];
if (ship == self->owner) {
continue;
}
float distance = vec3_len(vec3_sub(ship->position, self->position));
if (distance < 512) {
for (int p = 0; p < 32; p++) {
vec3_t velocity = vec3(rand_float(-512, 512), rand_float(-512, 512), rand_float(-512, 512));
velocity = vec3_add(velocity, vec3_mulf(ship->velocity, 0.25));
particles_spawn(self->position, self->ship_hit_particle, velocity, 256);
}
return ship;
}
}
return NULL;
}
bool weapon_collides_with_track(weapon_t *self) {
if (flags_is(self->section->flags, SECTION_JUMP)) {
return false;
}
track_face_t *face = g.track.faces + self->section->face_start;
for (int i = 0; i < self->section->face_count; i++) {
vec3_t face_point = face->tris[0].vertices[0].pos;
float distance = vec3_distance_to_plane(self->position, face_point, face->normal);
if (distance < 0) {
return true;
}
face++;
}
return false;
}
void weapon_update_wait_for_delay(weapon_t *self) {
if (self->timer <= 0) {
weapons_fire(self->owner, self->type);
self->active = false;
}
}
void weapon_fire_mine(ship_t *ship) {
float timer = 0;
for (int i = 0; i < WEAPON_MINE_COUNT; i++) {
weapon_t *self = weapon_init(ship);
if (!self) {
return;
}
timer += WEAPON_MINE_RELEASE_RATE;
self->timer = timer;
self->update_func = weapon_update_mine_wait_for_release;
}
}
void weapon_update_mine_wait_for_release(weapon_t *self) {
if (self->timer <= 0) {
self->timer = WEAPON_MINE_DURATION;
self->update_func = weapon_update_mine;
self->model = weapon_assets.mine;
self->position = self->owner->position;
self->angle.y = rand_float(0, M_PI * 2);
self->trail_particle = PARTICLE_TYPE_NONE;
self->track_hit_particle = PARTICLE_TYPE_NONE;
self->ship_hit_particle = PARTICLE_TYPE_FIRE;
if (self->owner->pilot == g.pilot) {
sfx_play(SFX_MINE_DROP);
}
}
}
void weapon_update_mine_lights(weapon_t *self, int index) {
Prm prm = {.primitive = self->model->primitives};
uint8_t r = sin(system_cycle_time() * M_PI * 2 + index * 0.66) * 128 + 128;
for (int i = 0; i < 8; i++) {
switch (prm.primitive->type) {
case PRM_TYPE_GT3:
prm.gt3->colour[0].as_rgba.r = 230;
prm.gt3->colour[1].as_rgba.r = r;
prm.gt3->colour[2].as_rgba.r = r;
prm.gt3->colour[0].as_rgba.g = 0;
prm.gt3->colour[1].as_rgba.g = 0x40;
prm.gt3->colour[2].as_rgba.g = 0x40;
prm.gt3->colour[0].as_rgba.b = 0;
prm.gt3->colour[1].as_rgba.b = 0;
prm.gt3->colour[2].as_rgba.b = 0;
prm.gt3 += 1;
break;
}
}
}
void weapon_update_mine(weapon_t *self) {
if (self->timer <= 0) {
self->active = false;
return;
}
// TODO: oscilate perpendicular to track!?
self->angle.y += system_tick();
ship_t *ship = weapon_collides_with_ship(self);
if (ship) {
sfx_play_at(SFX_EXPLOSION_1, self->position, vec3(0,0,0), 1);
self->active = false;
if (flags_not(ship->flags, SHIP_SHIELDED)) {
if (ship->pilot == g.pilot) {
ship->velocity = vec3_sub(ship->velocity, vec3_mulf(ship->velocity, 0.125));
// SetShake(20); // FIXME
}
else {
ship->speed = ship->speed * 0.125;
}
}
}
}
void weapon_fire_missile(ship_t *ship) {
weapon_t *self = weapon_init(ship);
if (!self) {
return;
}
self->timer = WEAPON_MISSILE_DURATION;
self->model = weapon_assets.missile;
self->update_func = weapon_update_missile;
self->trail_particle = PARTICLE_TYPE_SMOKE;
self->track_hit_particle = PARTICLE_TYPE_FIRE_WHITE;
self->ship_hit_particle = PARTICLE_TYPE_FIRE;
self->target = ship->weapon_target;
self->drag = 0.25;
weapon_set_trajectory(self);
if (self->owner->pilot == g.pilot) {
sfx_play(SFX_MISSILE_FIRE);
}
}
void weapon_update_missile(weapon_t *self) {
if (self->timer <= 0) {
self->active = false;
return;
}
weapon_follow_target(self);
// Collision with other ships
ship_t *ship = weapon_collides_with_ship(self);
if (ship) {
sfx_play_at(SFX_EXPLOSION_1, self->position, vec3(0,0,0), 1);
self->active = false;
if (flags_not(ship->flags, SHIP_SHIELDED)) {
if (ship->pilot == g.pilot) {
ship->velocity = vec3_sub(ship->velocity, vec3_mulf(ship->velocity, 0.75));
ship->angular_velocity.z += rand_float(-0.1, 0.1);
ship->turn_rate_from_hit = rand_float(-0.1, 0.1);
// SetShake(20); // FIXME
}
else {
ship->speed = ship->speed * 0.03125;
ship->angular_velocity.z += 10 * M_PI;
ship->turn_rate_from_hit = rand_float(-M_PI, M_PI);
}
}
}
}
void weapon_fire_rocket(ship_t *ship) {
weapon_t *self = weapon_init(ship);
if (!self) {
return;
}
self->timer = WEAPON_ROCKET_DURATION;
self->model = weapon_assets.rocket;
self->update_func = weapon_update_rocket;
self->trail_particle = PARTICLE_TYPE_SMOKE;
self->track_hit_particle = PARTICLE_TYPE_FIRE_WHITE;
self->ship_hit_particle = PARTICLE_TYPE_FIRE;
self->drag = 0.03125;
weapon_set_trajectory(self);
if (self->owner->pilot == g.pilot) {
sfx_play(SFX_MISSILE_FIRE);
}
}
void weapon_update_rocket(weapon_t *self) {
if (self->timer <= 0) {
self->active = false;
return;
}
// Collision with other ships
ship_t *ship = weapon_collides_with_ship(self);
if (ship) {
sfx_play_at(SFX_EXPLOSION_1, self->position, vec3(0,0,0), 1);
self->active = false;
if (flags_not(ship->flags, SHIP_SHIELDED)) {
if (ship->pilot == g.pilot) {
ship->velocity = vec3_sub(ship->velocity, vec3_mulf(ship->velocity, 0.75));
ship->angular_velocity.z += rand_float(-0.1, 0.1);;
ship->turn_rate_from_hit = rand_float(-0.1, 0.1);;
// SetShake(20); // FIXME
}
else {
ship->speed = ship->speed * 0.03125;
ship->angular_velocity.z += 10 * M_PI;
ship->turn_rate_from_hit = rand_float(-M_PI, M_PI);
}
}
}
}
void weapon_fire_ebolt(ship_t *ship) {
weapon_t *self = weapon_init(ship);
if (!self) {
return;
}
self->timer = WEAPON_EBOLT_DURATION;
self->model = weapon_assets.ebolt;
self->update_func = weapon_update_ebolt;
self->trail_particle = PARTICLE_TYPE_EBOLT;
self->track_hit_particle = PARTICLE_TYPE_EBOLT;
self->ship_hit_particle = PARTICLE_TYPE_GREENY;
self->target = ship->weapon_target;
self->drag = 0.25;
weapon_set_trajectory(self);
if (self->owner->pilot == g.pilot) {
sfx_play(SFX_EBOLT);
}
}
void weapon_update_ebolt(weapon_t *self) {
if (self->timer <= 0) {
self->active = false;
return;
}
weapon_follow_target(self);
// Collision with other ships
ship_t *ship = weapon_collides_with_ship(self);
if (ship) {
sfx_play_at(SFX_EXPLOSION_1, self->position, vec3(0,0,0), 1);
self->active = false;
if (flags_not(ship->flags, SHIP_SHIELDED)) {
flags_add(ship->flags, SHIP_ELECTROED);
ship->ebolt_timer = WEAPON_EBOLT_DURATION;
}
}
}
void weapon_fire_shield(ship_t *ship) {
weapon_t *self = weapon_init(ship);
if (!self) {
return;
}
self->timer = WEAPON_SHIELD_DURATION;
self->model = weapon_assets.shield;
self->update_func = weapon_update_shield;
flags_add(self->owner->flags, SHIP_SHIELDED);
}
void weapon_update_shield(weapon_t *self) {
if (self->timer <= 0) {
self->active = false;
flags_rm(self->owner->flags, SHIP_SHIELDED);
return;
}
if (flags_is(self->owner->flags, SHIP_VIEW_INTERNAL)) {
self->position = ship_cockpit(self->owner);
self->model = weapon_assets.shield_internal;
}
else {
self->position = self->owner->position;
self->model = weapon_assets.shield;
}
self->angle = self->owner->angle;
Prm poly = {.primitive = self->model->primitives};
int primitives_len = self->model->primitives_len;
uint8_t col0, col1, col2, col3;
int16_t *coords;
uint8_t shield_alpha = 48;
// FIXME: this looks kinda close to the PSX original!?
float color_timer = self->timer * 0.05;
for (int k = 0; k < primitives_len; k++) {
switch (poly.primitive->type) {
case PRM_TYPE_G3 :
coords = poly.g3->coords;
col0 = sin(color_timer * coords[0]) * 127 + 128;
col1 = sin(color_timer * coords[1]) * 127 + 128;
col2 = sin(color_timer * coords[2]) * 127 + 128;
poly.g3->colour[0].as_rgba.r = col0;
poly.g3->colour[0].as_rgba.g = col0;
poly.g3->colour[0].as_rgba.b = 255;
poly.g3->colour[0].as_rgba.a = shield_alpha;
poly.g3->colour[1].as_rgba.r = col1;
poly.g3->colour[1].as_rgba.g = col1;
poly.g3->colour[1].as_rgba.b = 255;
poly.g3->colour[1].as_rgba.a = shield_alpha;
poly.g3->colour[2].as_rgba.r = col2;
poly.g3->colour[2].as_rgba.g = col2;
poly.g3->colour[2].as_rgba.b = 255;
poly.g3->colour[2].as_rgba.a = shield_alpha;
poly.g3 += 1;
break;
case PRM_TYPE_G4 :
coords = poly.g4->coords;
col0 = sin(color_timer * coords[0]) * 127 + 128;
col1 = sin(color_timer * coords[1]) * 127 + 128;
col2 = sin(color_timer * coords[2]) * 127 + 128;
col3 = sin(color_timer * coords[3]) * 127 + 128;
poly.g4->colour[0].as_rgba.r = col0;
poly.g4->colour[0].as_rgba.g = col0;
poly.g4->colour[0].as_rgba.b = 255;
poly.g4->colour[0].as_rgba.a = shield_alpha;
poly.g4->colour[1].as_rgba.r = col1;
poly.g4->colour[1].as_rgba.g = col1;
poly.g4->colour[1].as_rgba.b = 255;
poly.g4->colour[1].as_rgba.a = shield_alpha;
poly.g4->colour[2].as_rgba.r = col2;
poly.g4->colour[2].as_rgba.g = col2;
poly.g4->colour[2].as_rgba.b = 255;
poly.g4->colour[2].as_rgba.a = shield_alpha;
poly.g4->colour[3].as_rgba.r = col3;
poly.g4->colour[3].as_rgba.g = col3;
poly.g4->colour[3].as_rgba.b = 255;
poly.g4->colour[3].as_rgba.a = shield_alpha;
poly.g4 += 1;
break;
}
}
}
void weapon_fire_turbo(ship_t *ship) {
ship->velocity = vec3_add(ship->velocity, vec3_mulf(ship->dir_forward, 39321)); // unitVecNose.vx) << 3) * FR60) / 50
if (ship->pilot == g.pilot) {
sfx_t *sfx = sfx_play(SFX_MISSILE_FIRE);
sfx->pitch = 0.25;
}
}
int weapon_get_random_type(int type_class) {
if (type_class == WEAPON_CLASS_ANY) {
int index = rand_int(0, 65);
if (index < 17) {
return WEAPON_TYPE_ROCKET;
}
else if (index < 35) {
return WEAPON_TYPE_MINE;
}
else if (index < 45) {
return WEAPON_TYPE_SHIELD;
}
else if (index < 53) {
return WEAPON_TYPE_MISSILE;
}
else if (index < 59) {
return WEAPON_TYPE_TURBO;
}
else {
return WEAPON_TYPE_EBOLT;
}
}
else if (type_class == WEAPON_CLASS_PROJECTILE) {
int index = rand_int(0, 60);
if (index < 27) {
return WEAPON_TYPE_ROCKET;
}
else if (index < 40) {
return WEAPON_TYPE_MISSILE;
}
else if (index < 50) {
return WEAPON_TYPE_TURBO;
}
else {
return WEAPON_TYPE_EBOLT;
}
}
else {
die("Unknown WEAPON_CLASS_ %d", type_class);
}
}

@ -0,0 +1,51 @@
#ifndef WEAPON_H
#define WEAPON_H
#define WEAPONS_MAX 64
#define WEAPON_MINE_DURATION (450 * (1.0/30.0))
#define WEAPON_ROCKET_DURATION (200 * (1.0/30.0))
#define WEAPON_EBOLT_DURATION (140 * (1.0/30.0))
#define WEAPON_REV_CON_DURATION (60 * (1.0/30.0))
#define WEAPON_MISSILE_DURATION (200 * (1.0/30.0))
#define WEAPON_SHIELD_DURATION (200 * (1.0/30.0))
#define WEAPON_FLARE_DURATION (200 * (1.0/30.0))
#define WEAPON_SPECIAL_DURATION (400 * (1.0/30.0))
#define WEAPON_MINE_RELEASE_RATE (3 * (1.0/30.0))
#define WEAPON_DELAY (40 * (1.0/30.0))
#define WEAPON_TYPE_NONE 0
#define WEAPON_TYPE_MINE 1
#define WEAPON_TYPE_MISSILE 2
#define WEAPON_TYPE_ROCKET 3
#define WEAPON_TYPE_SPECIAL 4
#define WEAPON_TYPE_EBOLT 5
#define WEAPON_TYPE_FLARE 6
#define WEAPON_TYPE_REV_CON 7
#define WEAPON_TYPE_SHIELD 8
#define WEAPON_TYPE_TURBO 9
#define WEAPON_TYPE_MAX 10
#define WEAPON_MINE_COUNT 5
#define WEAPON_HIT_NONE 0
#define WEAPON_HIT_SHIP 1
#define WEAPON_HIT_TRACK 2
#define WEAPON_PARTICLE_SPAWN_RATE 0.011
#define WEAPON_AI_DELAY 1.1
#define WEAPON_CLASS_ANY 1
#define WEAPON_CLASS_PROJECTILE 2
void weapons_load();
void weapons_init();
void weapons_fire(ship_t *ship, int weapon_type);
void weapons_fire_delayed(ship_t *ship, int weapon_type);
void weapons_update();
void weapons_draw();
int weapon_get_random_type(int type_class);
#endif

@ -0,0 +1,2 @@
replace this directory with the one from the wipeout NTSC version and copy
the common/ models from the PC version in.
Loading…
Cancel
Save