player input, gamepad, and replay improvements

may break existing replays
This commit is contained in:
Andrei "Akari" Alexeyev 2017-09-19 21:46:28 +03:00
parent c0e9d50cf4
commit d9611b0831
No known key found for this signature in database
GPG key ID: 048C3D2A5648B785
10 changed files with 255 additions and 139 deletions

View file

@ -36,7 +36,6 @@ typedef struct Dialog {
int page_time;
int birthtime;
bool skip;
} Dialog;
Dialog *create_dialog(char *left, char *right);

View file

@ -207,6 +207,24 @@ int gamepad_axis2gamekey(SDL_GameControllerAxis id, int val) {
return -1;
}
SDL_GameControllerAxis gamepad_gamekey2axis(KeyIndex key) {
switch(key) {
case KEY_UP: case KEY_DOWN: return config_get_int(CONFIG_GAMEPAD_AXIS_UD);
case KEY_LEFT: case KEY_RIGHT: return config_get_int(CONFIG_GAMEPAD_AXIS_LR);
default: return SDL_CONTROLLER_AXIS_INVALID;
}
}
int gamepad_gamekey2axisval(KeyIndex key) {
switch(key) {
case KEY_UP: return AXISVAL_UP;
case KEY_DOWN: return AXISVAL_DOWN;
case KEY_LEFT: return AXISVAL_LEFT;
case KEY_RIGHT: return AXISVAL_RIGHT;
default: return AXISVAL_NULL;
}
}
int gamepad_axis2menuevt(SDL_GameControllerAxis id, int val) {
if(id == SDL_CONTROLLER_AXIS_LEFTX || id == SDL_CONTROLLER_AXIS_RIGHTX)
return val == AXISVAL_LEFT ? E_CursorLeft : E_CursorRight;
@ -237,31 +255,71 @@ float gamepad_axis_sens(SDL_GameControllerAxis id) {
return 1.0;
}
static int gamepad_axis_process_value_deadzone(int raw) {
int val, vsign;
float deadzone = clamp(config_get_float(CONFIG_GAMEPAD_AXIS_DEADZONE), 0.01, 0.999);
int minval = clamp(deadzone, 0, 1) * GAMEPAD_AXIS_MAX;
val = raw;
vsign = sign(val);
val = abs(val);
if(val < minval) {
val = 0;
} else {
val = vsign * clamp((val - minval) / (1.0 - deadzone), 0, GAMEPAD_AXIS_MAX);
}
return val;
}
static int gamepad_axis_process_value(SDL_GameControllerAxis id, int raw) {
double sens = gamepad_axis_sens(id);
int sens_sign = sign(sens);
raw = gamepad_axis_process_value_deadzone(raw);
double x = raw / (double)GAMEPAD_AXIS_MAX;
int in_sign = sign(x);
x = pow(fabs(x), 1.0 / fabs(sens)) * in_sign * sens_sign;
x = x ? x : 0;
x = clamp(x * GAMEPAD_AXIS_MAX, GAMEPAD_AXIS_MIN, GAMEPAD_AXIS_MAX);
return (int)x;
}
int gamepad_get_player_axis_value(GamepadPlrAxis paxis) {
SDL_GameControllerAxis id;
if(!gamepad.initialized) {
return 0;
}
if(paxis == PLRAXIS_LR) {
id = config_get_int(CONFIG_GAMEPAD_AXIS_LR);
} else if(paxis == PLRAXIS_UD) {
id = config_get_int(CONFIG_GAMEPAD_AXIS_UD);
} else {
return INT_MAX;
}
return gamepad_axis_process_value(id, SDL_GameControllerGetAxis(gamepad.device, id));
}
void gamepad_axis(SDL_GameControllerAxis id, int raw, EventHandler handler, EventFlags flags, void *arg) {
signed char *a = gamepad.axis;
signed char val = AXISVAL(raw);
signed char val = AXISVAL(gamepad_axis_process_value_deadzone(raw));
bool free = config_get_int(CONFIG_GAMEPAD_AXIS_FREE);
bool menu = flags & EF_Menu;
bool game = flags & EF_Game;
bool gp = flags & EF_Gamepad;
//printf("axis: %i %i %i\n", id, val, raw);
if(game && free) {
int evt = gamepad_axis2gameevt(id);
if(evt >= 0) {
double sens = gamepad_axis_sens(id);
int sens_sign = sign(sens);
double x = raw / (double)GAMEPAD_AXIS_MAX;
int in_sign = sign(x);
x = pow(fabs(x), 1.0 / fabs(sens)) * in_sign * sens_sign;
x = x ? x : 0;
x = clamp(x * GAMEPAD_AXIS_MAX, GAMEPAD_AXIS_MIN, GAMEPAD_AXIS_MAX);
handler(evt, x, arg);
handler(evt, gamepad_axis_process_value(id, raw), arg);
}
}
@ -283,11 +341,12 @@ void gamepad_axis(SDL_GameControllerAxis id, int raw, EventHandler handler, Even
}
}
}
} else { // simulate release
} else if(a[id]) { // simulate release
if(game) {
int key = gamepad_axis2gamekey(id, a[id]);
handler(E_PlrKeyUp, key, arg);
}
a[id] = AXISVAL_NULL;
}
@ -353,25 +412,10 @@ void gamepad_event(SDL_Event *event, EventHandler handler, EventFlags flags, voi
if(!gamepad.initialized)
return;
int val;
int vsign;
float deadzone = clamp(config_get_float(CONFIG_GAMEPAD_AXIS_DEADZONE), 0, 0.999);
int minval = clamp(deadzone, 0, 1) * GAMEPAD_AXIS_MAX;
switch(event->type) {
case SDL_CONTROLLERAXISMOTION:
if(event->caxis.which == gamepad.instance) {
val = event->caxis.value;
vsign = sign(val);
val = abs(val);
if(val < minval) {
val = 0;
} else {
val = vsign * clamp((val - minval) / (1.0 - deadzone), 0, GAMEPAD_AXIS_MAX);
}
gamepad_axis(event->caxis.axis, val, handler, flags, arg);
gamepad_axis(event->caxis.axis, event->caxis.value, handler, flags, arg);
}
break;
@ -434,6 +478,14 @@ bool gamepad_gamekeypressed(KeyIndex key) {
if(!gamepad.initialized)
return false;
if(!config_get_int(CONFIG_GAMEPAD_AXIS_FREE)) {
SDL_GameControllerAxis axis = gamepad_gamekey2axis(key);
if(axis != SDL_CONTROLLER_AXIS_INVALID && gamepad.axis[axis] == gamepad_gamekey2axisval(key)) {
return true;
}
}
int gpkey = config_gamepad_key_from_key(key);
if(gpkey < 0) {

View file

@ -14,6 +14,21 @@
#include "events.h"
#include "config.h"
enum {
AXISVAL_LEFT = -1,
AXISVAL_RIGHT = 1,
AXISVAL_UP = -1,
AXISVAL_DOWN = 1,
AXISVAL_NULL = 0
};
typedef enum {
PLRAXIS_LR, // aka X
PLRAXIS_UD, // aka Y
} GamepadPlrAxis;
void gamepad_init(void);
void gamepad_shutdown(void);
void gamepad_restart(void);
@ -32,21 +47,7 @@ bool gamepad_gamekeypressed(KeyIndex key);
const char* gamepad_button_name(SDL_GameControllerButton btn);
const char* gamepad_axis_name(SDL_GameControllerAxis btn);
// shitty workaround for the options menu. Used to list devices while the gamepad subsystem is off.
// only initializes the SDL subsystem so you can use gamepad_devicecount/gamepad_devicename.
// if gamepad has been initialized already, these do nothing.
void gamepad_init_bare(void);
void gamepad_shutdown_bare(void);
enum {
AXISVAL_LEFT = -1,
AXISVAL_RIGHT = 1,
AXISVAL_UP = -1,
AXISVAL_DOWN = 1,
AXISVAL_NULL = 0
};
int gamepad_get_player_axis_value(GamepadPlrAxis paxis);
#define GAMEPAD_AXIS_MAX 32767
#define GAMEPAD_AXIS_MIN -32768

View file

@ -763,7 +763,7 @@ void draw_options_menu(MenuData *menu) {
case BT_GamepadDevice: {
if(bind_isactive(bind)) {
// XXX: I'm not exactly a huge fan of fixing up state in drawing code, but it seems the way to go for now...
bind->valrange_max = gamepad_devicecount() - 1;
bind->valrange_max = gamepad_devicecount();
if(bind->selected < 0 || bind->selected > bind->valrange_max) {
bind->selected = gamepad_currentdevice();

View file

@ -14,6 +14,8 @@
#include "stage.h"
#include "stagetext.h"
#include <SDL_bits.h>
void init_player(Player *plr) {
memset(plr, 0, sizeof(Player));
plr->pos = VIEWPORT_W/2 + I*(VIEWPORT_H-64);
@ -54,7 +56,7 @@ static void player_full_power(Player *plr) {
stagetext_add("Full Power!", VIEWPORT_W * 0.5 + VIEWPORT_H * 0.33 * I, AL_Center, _fonts.mainmenu, rgb(1, 1, 1), 0, 60, 20, 20);
}
void player_set_power(Player *plr, short npow, bool handle_fullpower) {
bool player_set_power(Player *plr, short npow, bool handle_fullpower) {
npow = clamp(npow, 0, PLR_MAX_POWER);
switch(plr->cha) {
@ -72,6 +74,8 @@ void player_set_power(Player *plr, short npow, bool handle_fullpower) {
if(plr->power == PLR_MAX_POWER && oldpow < PLR_MAX_POWER && handle_fullpower) {
player_full_power(plr);
}
return oldpow != plr->power;
}
void player_move(Player *plr, complex delta) {
@ -203,9 +207,9 @@ void player_logic(Player* plr) {
}
}
void player_bomb(Player *plr) {
bool player_bomb(Player *plr) {
if(global.boss && global.boss->current && global.boss->current->type == AT_ExtraSpell)
return;
return false;
if(global.frames - plr->recovery >= 0 && (plr->bombs > 0 || plr->iddqd) && global.frames - plr->respawntime >= 60) {
player_fail_spell(plr);
@ -234,7 +238,11 @@ void player_bomb(Player *plr) {
}
plr->recovery = global.frames + BOMB_RECOVERY;
return true;
}
return false;
}
void player_realdeath(Player *plr) {
@ -290,37 +298,74 @@ static PlrInputFlag key_to_inflag(KeyIndex key) {
case KEY_RIGHT: return INFLAG_RIGHT; break;
case KEY_FOCUS: return INFLAG_FOCUS; break;
case KEY_SHOT: return INFLAG_SHOT; break;
case KEY_SKIP: return INFLAG_SKIP; break;
default: return 0;
}
}
void player_setinputflag(Player *plr, KeyIndex key, bool mode) {
PlrInputFlag flag = key_to_inflag(key);
bool player_updateinputflags(Player *plr, PlrInputFlag flags) {
if(flags == plr->inputflags) {
return false;
}
if(!flag) {
return;
PlrInputFlag newmove = INFLAGS_MOVE & flags & ~plr->inputflags;
if(newmove) {
plr->prevmove = plr->curmove;
plr->prevmovetime = plr->movetime;
plr->curmove = (1 << (unsigned)SDL_MostSignificantBitIndex32(flags));
plr->movetime = global.frames;
}
plr->inputflags = flags;
return true;
}
bool player_updateinputflags_moveonly(Player *plr, PlrInputFlag flags) {
return player_updateinputflags(plr, (flags & INFLAGS_MOVE) | (plr->inputflags & ~INFLAGS_MOVE));
}
bool player_setinputflag(Player *plr, KeyIndex key, bool mode) {
PlrInputFlag newflags = plr->inputflags;
PlrInputFlag keyflag = key_to_inflag(key);
if(!keyflag) {
return false;
}
if(mode) {
if(flag & INFLAGS_MOVE) {
plr->prevmove = plr->curmove;
plr->prevmovetime = plr->movetime;
plr->curmove = flag;
plr->movetime = global.frames;
}
plr->inputflags |= flag;
newflags |= keyflag;
} else {
plr->inputflags &= ~flag;
newflags &= ~keyflag;
}
return player_updateinputflags(plr, newflags);
}
void player_event(Player* plr, int type, int key) {
static bool player_set_axis(int *aptr, uint16_t value) {
int16_t new = (int16_t)value;
if(*aptr == new) {
return false;
}
*aptr = new;
return true;
}
bool player_event(Player *plr, uint8_t type, uint16_t value) {
bool useful = true;
switch(type) {
case EV_PRESS:
switch(key) {
if(global.dialog && (value == KEY_SHOT || value == KEY_BOMB)) {
page_dialog(&global.dialog);
break;
}
switch(value) {
case KEY_BOMB:
player_bomb(plr);
useful = player_bomb(plr);
break;
case KEY_IDDQD:
@ -328,35 +373,55 @@ void player_event(Player* plr, int type, int key) {
break;
case KEY_POWERUP:
player_set_power(plr, plr->power + 100,true);
useful = player_set_power(plr, plr->power + 100, true);
break;
case KEY_POWERDOWN:
player_set_power(plr, plr->power - 100,true);
useful = player_set_power(plr, plr->power - 100, true);
break;
default:
player_setinputflag(plr, key, true);
useful = player_setinputflag(plr, value, true);
break;
}
break;
case EV_RELEASE:
player_setinputflag(plr, key, false);
player_setinputflag(plr, value, false);
break;
case EV_AXIS_LR:
plr->axis_lr = key;
useful = player_set_axis(&plr->axis_lr, value);
break;
case EV_AXIS_UD:
plr->axis_ud = key;
useful = player_set_axis(&plr->axis_ud, value);
break;
case EV_INFLAGS:
useful = player_updateinputflags(plr, value);
break;
default:
log_warn("Unknown event type %d (value=%d)", type, key);
log_warn("Can not handle event: [%i:%02x:%04x]", global.frames, type, value);
useful = false;
break;
}
return useful;
}
bool player_event_with_replay(Player *plr, uint8_t type, uint16_t value) {
assert(global.replaymode == REPLAY_RECORD);
if(player_event(plr, type, value)) {
replay_stage_event(global.replay_stage, global.frames, type, value);
return true;
} else {
log_debug("Useless event discarded: [%i:%02x:%04x]", global.frames, type, value);
}
return false;
}
// free-axis movement
@ -376,10 +441,12 @@ bool player_applymovement_gamepad(Player *plr) {
int sr = sign(creal(direction));
int si = sign(cimag(direction));
player_setinputflag(plr, KEY_UP, si == -1);
player_setinputflag(plr, KEY_DOWN, si == 1);
player_setinputflag(plr, KEY_LEFT, sr == -1);
player_setinputflag(plr, KEY_RIGHT, sr == 1);
player_updateinputflags_moveonly(plr,
(INFLAG_UP * (si == -1)) |
(INFLAG_DOWN * (si == 1)) |
(INFLAG_LEFT * (sr == -1)) |
(INFLAG_RIGHT * (sr == 1))
);
if(direction) {
plr->gamepadmove = true;
@ -426,30 +493,41 @@ void player_applymovement(Player *plr) {
player_move(&global.plr, direction);
}
void player_input_workaround(Player *plr) {
if(global.dialog)
return;
void player_fix_input(Player *plr) {
// correct input state to account for any events we might have missed,
// usually because the pause menu ate them up
PlrInputFlag newflags = plr->inputflags;
for(KeyIndex key = KEYIDX_FIRST; key <= KEYIDX_LAST; ++key) {
int flag = key_to_inflag(key);
if(flag) {
int event = -1;
if(flag && !(plr->gamepadmove && (flag & INFLAGS_MOVE))) {
bool flagset = plr->inputflags & flag;
bool keyheld = gamekeypressed(key);
if(flagset && !keyheld) {
// something ate the release event (most likely the ingame menu)
event = EV_RELEASE;
newflags &= ~flag;
} else if(!flagset && keyheld) {
// something ate the press event (most likely the ingame menu)
event = EV_PRESS;
newflags |= flag;
}
}
}
if(event != -1) {
player_event(plr, event, key);
replay_stage_event(global.replay_stage, global.frames, event, key);
}
if(newflags != plr->inputflags) {
player_event_with_replay(plr, EV_INFLAGS, newflags);
}
if(config_get_int(CONFIG_GAMEPAD_AXIS_FREE)) {
int axis_lr = gamepad_get_player_axis_value(PLRAXIS_LR);
int axis_ud = gamepad_get_player_axis_value(PLRAXIS_UD);
if(plr->axis_lr != axis_lr) {
player_event_with_replay(plr, EV_AXIS_LR, axis_lr);
}
if(plr->axis_ud != axis_ud) {
player_event_with_replay(plr, EV_AXIS_UD, axis_ud);
}
}
}

View file

@ -48,6 +48,7 @@ typedef enum {
INFLAG_RIGHT = 8,
INFLAG_FOCUS = 16,
INFLAG_SHOT = 32,
INFLAG_SKIP = 64,
} PlrInputFlag;
enum {
@ -119,6 +120,7 @@ enum {
EV_AXIS_UD,
EV_CHECK_DESYNC, // replay-only
EV_FPS, // replay-only
EV_INFLAGS,
};
void init_player(Player*);
@ -128,19 +130,18 @@ void player_draw(Player*);
void player_logic(Player*);
void player_set_char(Player*, Character);
void player_set_power(Player *plr, short npow, bool handle_fullpower);
bool player_set_power(Player *plr, short npow, bool handle_fullpower);
void player_move(Player*, complex delta);
void player_bomb(Player*);
void player_realdeath(Player*);
void player_death(Player*);
void player_graze(Player*, complex, int);
void player_setinputflag(Player *plr, KeyIndex key, bool mode);
void player_event(Player* plr, int type, int key);
bool player_event(Player* plr, uint8_t type, uint16_t value);
bool player_event_with_replay(Player *plr, uint8_t type, uint16_t value);
void player_applymovement(Player* plr);
void player_input_workaround(Player *plr);
void player_fix_input(Player *plr);
void player_add_life_fragments(Player *plr, int frags);
void player_add_bomb_fragments(Player *plr, int frags);

View file

@ -106,17 +106,16 @@ void replay_destroy(Replay *rpy) {
log_debug("Replay at %p destroyed", (void*)rpy);
}
void replay_stage_event(ReplayStage *stg, uint32_t frame, uint8_t type, int16_t value) {
void replay_stage_event(ReplayStage *stg, uint32_t frame, uint8_t type, uint16_t value) {
if(!stg) {
return;
}
ReplayStage *s = stg;
ReplayEvent *e = s->events + s->numevents;
ReplayEvent *e = s->events + s->numevents++;
e->frame = frame;
e->type = type;
e->value = (uint16_t)value;
s->numevents++;
e->value = value;
if(s->numevents >= s->capacity) {
log_debug("Replay stage reached its capacity of %d, reallocating", s->capacity);
@ -587,6 +586,7 @@ void replay_stage_check_desync(ReplayStage *stg, int time, uint16_t check, Repla
if(mode == REPLAY_PLAY) {
if(stg->desync_check && stg->desync_check != check) {
log_warn("Replay desync detected! %u != %u", stg->desync_check, check);
stg->desynced = true;
} else {
log_debug("%u OK", check);
}

View file

@ -37,8 +37,9 @@
#define REPLAY_EXTENSION "tsr"
#define REPLAY_USELESS_BYTE 0x69
#define REPLAY_WRITE_DESYNC_CHECKS
#ifdef DEBUG
#define REPLAY_WRITE_DESYNC_CHECKS
#define REPLAY_LOAD_GARBAGE_TEST
#endif
@ -91,6 +92,7 @@ typedef struct ReplayStage {
int playpos;
int fps;
uint16_t desync_check;
bool desynced;
} ReplayStage;
typedef struct Replay {
@ -147,7 +149,7 @@ ReplayStage* replay_create_stage(Replay *rpy, StageInfo *stage, uint64_t seed, D
void replay_destroy(Replay *rpy);
void replay_destroy_events(Replay *rpy);
void replay_stage_event(ReplayStage *stg, uint32_t frame, uint8_t type, int16_t value);
void replay_stage_event(ReplayStage *stg, uint32_t frame, uint8_t type, uint16_t value);
void replay_stage_check_desync(ReplayStage *stg, int time, uint16_t check, ReplayMode mode);
void replay_stage_sync_player_state(ReplayStage *stg, Player *plr);

View file

@ -263,25 +263,11 @@ void stage_input_event(EventType type, int key, void *arg) {
break;
#endif
if(global.dialog && (key == KEY_SHOT || key == KEY_BOMB)) {
page_dialog(&global.dialog);
replay_stage_event(global.replay_stage, global.frames, EV_PRESS, key);
} else {
player_event(&global.plr, EV_PRESS, key);
replay_stage_event(global.replay_stage, global.frames, EV_PRESS, key);
if(key == KEY_SKIP && global.dialog) {
global.dialog->skip = true;
}
}
player_event_with_replay(&global.plr, EV_PRESS, key);
break;
case E_PlrKeyUp:
player_event(&global.plr, EV_RELEASE, key);
replay_stage_event(global.replay_stage, global.frames, EV_RELEASE, key);
if(key == KEY_SKIP && global.dialog)
global.dialog->skip = false;
player_event_with_replay(&global.plr, EV_RELEASE, key);
break;
case E_Pause:
@ -289,13 +275,11 @@ void stage_input_event(EventType type, int key, void *arg) {
break;
case E_PlrAxisLR:
player_event(&global.plr, EV_AXIS_LR, key);
replay_stage_event(global.replay_stage, global.frames, EV_AXIS_LR, key);
player_event_with_replay(&global.plr, EV_AXIS_LR, (uint16_t)key);
break;
case E_PlrAxisUD:
player_event(&global.plr, EV_AXIS_UD, key);
replay_stage_event(global.replay_stage, global.frames, EV_AXIS_UD, key);
player_event_with_replay(&global.plr, EV_AXIS_UD, (uint16_t)key);
break;
default: break;
@ -333,12 +317,7 @@ void replay_input(void) {
break;
default:
if(global.dialog && e->type == EV_PRESS && (e->value == KEY_SHOT || e->value == KEY_BOMB))
page_dialog(&global.dialog);
else if(global.dialog && (e->type == EV_PRESS || e->type == EV_RELEASE) && e->value == KEY_SKIP)
global.dialog->skip = (e->type == EV_PRESS);
else
player_event(&global.plr, e->type, (int16_t)e->value);
player_event(&global.plr, e->type, (int16_t)e->value);
break;
}
}
@ -349,14 +328,7 @@ void replay_input(void) {
void stage_input(void) {
handle_events(stage_input_event, EF_Game, NULL);
// workaround
if(global.dialog && global.dialog->skip && !gamekeypressed(KEY_SKIP)) {
global.dialog->skip = false;
replay_stage_event(global.replay_stage, global.frames, EV_RELEASE, KEY_SKIP);
}
player_input_workaround(&global.plr);
player_fix_input(&global.plr);
player_applymovement(&global.plr);
}
@ -372,7 +344,7 @@ static void stage_logic(void) {
if(global.boss && !global.dialog)
process_boss(&global.boss);
if(global.dialog && global.dialog->skip && global.frames - global.dialog->page_time > 3)
if(global.dialog && (global.plr.inputflags & INFLAG_SKIP) && global.frames - global.dialog->page_time > 3)
page_dialog(&global.dialog);
global.frames++;

View file

@ -502,9 +502,20 @@ void stage_draw_hud(void) {
if(global.replaymode == REPLAY_PLAY) {
snprintf(buf, sizeof(buf), "Replay: %s (%i fps)", global.replay.playername, global.replay_stage->fps);
glColor4f(0.5f, 0.5f, 0.5f, 0.5f);
draw_text(AL_Left, 0, SCREEN_H - 0.5 * stringheight(buf, _fonts.standard), buf, _fonts.standard);
int x = 0, y = SCREEN_H - 0.5 * stringheight(buf, _fonts.standard);
glColor4f(0.5f, 0.5f, 0.5f, 0.6f);
draw_text(AL_Left, x, y, buf, _fonts.standard);
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
if(global.replay_stage->desynced) {
x += stringwidth(buf, _fonts.standard);
strlcpy(buf, " (DESYNCED)", sizeof(buf));
glColor4f(1.0f, 0.2f, 0.2f, 0.6f);
draw_text(AL_Left, x, y, buf, _fonts.standard);
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
}
}
#ifdef PLR_DPS_STATS
else {