Files
2025-12-07 11:32:06 +01:00

1356 lines
37 KiB
C

#define _NO_CRT_STDIO_INLINE
#include <SDL.h>
#include <SDL_mixer.h>
#if SDL_MAJOR_VERSION >= 2
#include "sdl20compat.inc.c"
#endif
#include <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include <time.h>
#ifdef _3DS
#include <3ds.h>
#endif
#include "celeste.h"
#include "tilemap.h"
#define ErrLog SDL_Log
SDL_Window* window;
SDL_Renderer* renderer;
SDL_Texture* texture;
SDL_Surface* screen = NULL;
SDL_Rect screen_dstRect;
SDL_Surface* gfx = NULL;
SDL_Surface* font = NULL;
Mix_Chunk* snd[64] = { NULL };
Mix_Music* mus[6] = { NULL };
#define PICO8_W 128
#define PICO8_H 128
static int scale = 1;
static const SDL_Color base_palette[16] = {
{0x00, 0x00, 0x00},
{0x1d, 0x2b, 0x53},
{0x7e, 0x25, 0x53},
{0x00, 0x87, 0x51},
{0xab, 0x52, 0x36},
{0x5f, 0x57, 0x4f},
{0xc2, 0xc3, 0xc7},
{0xff, 0xf1, 0xe8},
{0xff, 0x00, 0x4d},
{0xff, 0xa3, 0x00},
{0xff, 0xec, 0x27},
{0x00, 0xe4, 0x36},
{0x29, 0xad, 0xff},
{0x83, 0x76, 0x9c},
{0xff, 0x77, 0xa8},
{0xff, 0xcc, 0xaa}
};
static SDL_Color palette[16];
static inline Uint32 getcolor(char idx) {
SDL_Color c = palette[idx % 16];
return SDL_MapRGB(screen->format, c.r, c.g, c.b);
}
static void ResetPalette(void) {
//SDL_SetPalette(surf, SDL_PHYSPAL|SDL_LOGPAL, (SDL_Color*)base_palette, 0, 16);
//memcpy(screen->format->palette->colors, base_palette, 16*sizeof(SDL_Color));
memcpy(palette, base_palette, sizeof palette);
}
static char* GetDataPath(char* path, int n, const char* fname) {
#ifdef _3DS
snprintf(path, n, "romfs:/%s", fname);
#else
#ifdef _WIN32 || defined(__XBOX__)
char pathsep = '\\';
#else
char pathsep = '/';
#endif //_WIN32
#ifdef __XBOX__
sprintf(path, "D:\\Media\\data%c%s", pathsep, fname);
#else
snprintf(path, n, "data%c%s", pathsep, fname);
#endif // __XBOX__
#endif //_3DS
return path;
}
static Uint32 getpixel(SDL_Surface* surface, int x, int y) {
int bpp = surface->format->BytesPerPixel;
/* Here p is the address to the pixel we want to retrieve */
Uint8* p = (Uint8*)surface->pixels + y * surface->pitch + x * bpp;
switch (bpp) {
case 1:
return *p;
case 2:
return *(Uint16*)p;
case 3:
if (SDL_BYTEORDER == SDL_BIG_ENDIAN)
return p[0] << 16 | p[1] << 8 | p[2];
else
return p[0] | p[1] << 8 | p[2] << 16;
case 4:
return *(Uint32*)p;
}
return 0;
}
static void loadbmpscale(char* filename, SDL_Surface** s) {
SDL_Surface* surf = *s;
if (surf) SDL_FreeSurface(surf), surf = *s = NULL;
char tmpath[4096];
SDL_Surface* bmp = SDL_LoadBMP(GetDataPath(tmpath, sizeof tmpath, filename));
if (!bmp) {
ErrLog("error loading bmp '%s': %s\n", filename, SDL_GetError());
return;
}
int w = bmp->w, h = bmp->h;
surf = SDL_CreateRGBSurface(SDL_SWSURFACE, w * scale, h * scale, 8, 0, 0, 0, 0);
assert(surf != NULL);
unsigned char* data = surf->pixels;
/*memcpy((_S)->format->palette->colors, base_palette, 16*sizeof(SDL_Color));*/
for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) {
unsigned char pix = getpixel(bmp, x, y);
for (int i = 0; i < scale; i++) for (int j = 0; j < scale; j++) {
data[x * scale + i + (y * scale + j) * w * scale] = pix;
}
}
SDL_FreeSurface(bmp);
SDL_SetPalette(surf, SDL_PHYSPAL | SDL_LOGPAL, (SDL_Color*)base_palette, 0, 16);
SDL_SetColorKey(surf, SDL_SRCCOLORKEY, 0);
*s = surf;
}
#ifdef _DEBUG
#define LOGLOAD(w) SDL_Log("loading %s...", w)
#define LOGDONE() SDL_Log("done\n")
#else
#define LOGLOAD(w)
#define LOGDONE()
#endif
static void LoadData(void) {
LOGLOAD("gfx.bmp");
loadbmpscale("gfx.bmp", &gfx);
LOGDONE();
LOGLOAD("font.bmp");
loadbmpscale("font.bmp", &font);
LOGDONE();
static const char sndids[] = { 0,1,2,3,4,5,6,7,8,9,13,14,15,16,23,35,37,38,40,50,51,54,55 };
for (int iid = 0; iid < sizeof sndids; iid++) {
const int id = sndids[iid];
char fname[20];
sprintf(fname, "snd%i.wav", id);
char path[4096];
LOGLOAD(fname);
GetDataPath(path, sizeof path, fname);
snd[id] = Mix_LoadWAV(path);
if (!snd[id]) {
ErrLog("snd%i: Mix_LoadWAV: %s\n", id, Mix_GetError());
}
LOGDONE();
}
static const char musids[] = { 0,10,20,30,40 };
for (int iid = 0; iid < sizeof musids; iid++) {
const int id = musids[iid];
char fname[20];
sprintf(fname, "mus%i.ogg", id);
LOGLOAD(fname);
char path[4096];
GetDataPath(path, sizeof path, fname);
mus[id / 10] = Mix_LoadMUS(path);
if (!mus[id / 10]) {
ErrLog("mus%i: Mix_LoadMUS: %s\n", id, Mix_GetError());
}
LOGDONE();
}
}
static Uint16 buttons_state = 0;
#define SDL_CHECK(r) do { \
if (!(r)) { \
ErrLog("%s:%i, fatal error: `%s` -> %s\n", \
__FILE__, __LINE__, #r, SDL_GetError()); \
exit(2); \
} \
} while(0)
static void p8_rectfill(int x0, int y0, int x1, int y1, int col);
static void p8_print(const char* str, int x, int y, int col);
//on-screen display (for info, such as loading a state, toggling screenshake, toggling fullscreen, etc)
static char osd_text[200] = "";
static int osd_timer = 0;
static void OSDset(const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
_vsnprintf(osd_text, sizeof osd_text, fmt, ap);
osd_text[sizeof osd_text - 1] = '\0'; //make sure to add NUL terminator in case of truncation
osd_timer = 30;
va_end(ap);
}
static void OSDdraw(void) {
if (osd_timer > 0) {
--osd_timer;
//HCF
//const int x = 64 + 4;
const int x = 4;
const int y = 120 + (osd_timer < 10 ? 10 - osd_timer : 0); //disappear by going below the screen
p8_rectfill(x - 2, y - 2, x + 4 * strlen(osd_text), y + 6, 6); //outline
p8_rectfill(x - 1, y - 1, x + 4 * strlen(osd_text) - 1, y + 5, 0);
p8_print(osd_text, x, y, 7);
}
}
static void DrawPausedBanner(void) {
const char* t = "paused";
const int tw = 4 * (int)strlen(t); // p8_print is 4px per char
const int x0 = (PICO8_W - tw) / 2;
const int y0 = 8;
p8_rectfill(x0 - 1, y0 - 1, x0 + tw + 1, y0 + 6 + 1, 6);
p8_rectfill(x0, y0, x0 + tw, y0 + 6, 0);
p8_print(t, x0 + 1, y0 + 1, 7);
}
static Mix_Music* current_music = NULL;
static _Bool enable_screenshake = 1;
static _Bool paused = 0;
static _Bool running = 1;
static void* initial_game_state = NULL;
static void* game_state = NULL;
static Mix_Music* game_state_music = NULL;
static void mainLoop(void);
static FILE* TAS = NULL;
// TODO: Clean up
int SCREEN_WIDTH = 1920;
int SCREEN_HEIGHT = 1080;
// === scaling & pause menu state ===
enum { SCALE_INT = 0, SCALE_ASPECT = 1, SCALE_STRETCH = 2 };
static int screen_scaling_mode = SCALE_INT; // runtime-selected scaling
static _Bool pause_menu_active = 0;
static int pause_menu_index = 0; // 0: Scaling row, 1: Apply, 2: Resume
static int pause_scaling_choice = 0; // temp choice while paused
static int pause_screenshake_choice = 0; // temp choice while paused
static const char* scaling_names[] = { "integer", "aspect", "stretched" };
static const char* screenshake_names[] = { "on", "off" };
SDL_Rect get_integer_scale_rectangle() {
// Calculate the maximum integer scale that fits within the target resolution
int scale_x = SCREEN_WIDTH / (PICO8_W * scale);
int scale_y = SCREEN_HEIGHT / (PICO8_H * scale);
int xscale = min(scale_x, scale_y);
// Calculate the scaled dimensions
int scaled_width = (PICO8_W * scale) * xscale;
int scaled_height = (PICO8_H * scale) * xscale;
// Calculate padding to center the image
int pad_x = (SCREEN_WIDTH - scaled_width) / 2;
int pad_y = (SCREEN_HEIGHT - scaled_height) / 2;
SDL_Rect dstRect = { pad_x, pad_y, scaled_width, scaled_height };
return dstRect;
}
SDL_Rect get_streteched_aspect_rectangle() {
// uniform scale that fits inside the window
float sx = (float)SCREEN_WIDTH / (PICO8_W * scale);
float sy = (float)SCREEN_HEIGHT / (PICO8_H * scale);
float s = (sx < sy) ? sx : sy;
int w = (int)((PICO8_W * scale) * s + 0.5f);
int h = (int)((PICO8_H * scale) * s + 0.5f);
int x = (SCREEN_WIDTH - w) / 2;
int y = (SCREEN_HEIGHT - h) / 2;
SDL_Rect dstRect = { x, y, w, h };
return dstRect;
}
SDL_Rect get_streteched_rectangle() {
SDL_Rect dstRect = { 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT };
return dstRect;
}
static void RecomputeScreenDstRect(void) {
switch (screen_scaling_mode) {
case SCALE_INT: screen_dstRect = get_integer_scale_rectangle(); break;
case SCALE_ASPECT: screen_dstRect = get_streteched_aspect_rectangle(); break;
case SCALE_STRETCH: screen_dstRect = get_streteched_rectangle(); break;
default: screen_dstRect = get_integer_scale_rectangle(); break;
}
}
int mainy() { //int argc, char** argv) {
SDL_CHECK(SDL_Init(SDL_INIT_AUDIO | SDL_INIT_VIDEO) == 0);
#if SDL_MAJOR_VERSION >= 2
//HCF
SDL_InitSubSystem(SDL_INIT_JOYSTICK);
/*
SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER);
SDL_GameControllerAddMappingsFromRW(SDL_RWFromFile("D:\\gamecontrollerdb.txt", "rb"), 1);
*/
#endif
int videoflag = SDL_SWSURFACE | SDL_HWPALETTE;
#
//HCF
//SDL_CHECK(screenReal = SDL_SetVideoMode(640, 480, 32, videoflag));
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0"); // "nearest" / "0" / "1" / "2"
// 1) Create window
window = SDL_CreateWindow("Celeste",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
SCREEN_WIDTH, SCREEN_HEIGHT, 0);
// 2) Create renderer
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
// 3) Use renderer's real backbuffer size as the source of truth
int rw = 0, rh = 0;
if (renderer && SDL_GetRendererOutputSize(renderer, &rw, &rh) == 0) {
SCREEN_WIDTH = rw;
SCREEN_HEIGHT = rh;
}
else {
SDL_GetWindowSize(window, &SCREEN_WIDTH, &SCREEN_HEIGHT);
SDL_Log("GetRendererOutputSize failed (%s); using window size", SDL_GetError());
}
// 4) Now it's safe to compute rectangles and create textures that depend on size
RecomputeScreenDstRect();
//OSDset("window=%dx%d, renderer=%dx%d", SCREEN_WIDTH, SCREEN_HEIGHT, rw, rh);
// seed pause menu
pause_scaling_choice = screen_scaling_mode;
pause_screenshake_choice = enable_screenshake ? 0 : 1;
screen = SDL_CreateRGBSurface(
0, // flags
PICO8_W * scale, // weight
PICO8_H * scale, // height
32, // bit-depth
0x00FF0000, // Red
0x0000FF00, // Green
0x000000FF, // Blue
0xFF000000 // Alfa
);
int mixflag = MIX_INIT_OGG;
if (Mix_Init(mixflag) != mixflag) {
ErrLog("Mix_Init: %s\n", Mix_GetError());
return -1;
}
if (Mix_OpenAudio(22050, AUDIO_S16SYS, 1, 1024) < 0) {
ErrLog("Mix_Init: %s\n", Mix_GetError());
return -1;
}
ResetPalette();
SDL_ShowCursor(0);
#ifdef _DEBUG
SDL_Log("game state size %gkb\n", Celeste_P8_get_state_size()/1024.);
SDL_Log("now loading...\n");
#endif
{
const unsigned char loading_bmp[] = {
0x42,0x4d,0xca,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x82,0x00,
0x00,0x00,0x6c,0x00,0x00,0x00,0x24,0x00,0x00,0x00,0x09,0x00,
0x00,0x00,0x01,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x48,0x00,
0x00,0x00,0x23,0x2e,0x00,0x00,0x23,0x2e,0x00,0x00,0x02,0x00,
0x00,0x00,0x02,0x00,0x00,0x00,0x42,0x47,0x52,0x73,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0x00,0x00,0x00,
0x00,0x00,0xe0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x00,
0x00,0x00,0x66,0x3e,0xf1,0x24,0xf0,0x00,0x00,0x00,0x49,0x44,
0x92,0x24,0x90,0x00,0x00,0x00,0x49,0x3c,0x92,0x24,0x90,0x00,
0x00,0x00,0x49,0x04,0x92,0x24,0x90,0x00,0x00,0x00,0x46,0x38,
0xf0,0x3c,0xf0,0x00,0x00,0x00,0x40,0x00,0x12,0x00,0x00,0x00,
0x00,0x00,0xc0,0x00,0x10,0x00,0x00,0x00,0x00,0x00
};
SDL_RWops* rw = SDL_RWFromConstMem(loading_bmp, sizeof loading_bmp);
SDL_Surface* loading = SDL_LoadBMP_RW(rw, 1);
if (!loading) goto skip_load;
//HCF
//SDL_Rect rc = { 64+60, 60 };
SDL_Rect rc = { 60, 60 };
SDL_BlitSurface(loading, NULL, screen, &rc);
//HCF
rc.x = 64;
rc.y = 0;
/*
rc.x = 128;
rc.y = 48;
*/
/*
SDL_Rect roc;
roc.x = 0;
roc.y = 16;
roc.w = screen->w;
roc.h = screen->h - 16;
SDL_BlitSurface(screen, &roc, screenReal, &rc);
*/
//SDL_FillRect(screenReal, NULL, SDL_MapRGB(screenReal->format, 0, 0, 0));
//SDL_BlitSurface(screen, NULL, screenReal, &rc);
//SDL_Flip(screenReal);
// Convertir a textura
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, screen);
SDL_RenderClear(renderer);
// Renderizar la textura escalada
SDL_RenderCopy(renderer, texture, NULL, &screen_dstRect);
SDL_RenderPresent(renderer);
SDL_DestroyTexture(texture);
SDL_FreeSurface(loading);
} skip_load:
LoadData();
int pico8emu(CELESTE_P8_CALLBACK_TYPE call, ...);
Celeste_P8_set_call_func(pico8emu);
//for reset
initial_game_state = SDL_malloc(Celeste_P8_get_state_size());
if (initial_game_state) Celeste_P8_save_state(initial_game_state);
if (TAS) {
// a consistent seed for tas playback
Celeste_P8_set_rndseed(8);
}
else {
Celeste_P8_set_rndseed((unsigned)(time(NULL) + SDL_GetTicks()));
}
Celeste_P8_init();
#ifdef _3DS
while (aptMainLoop()) mainLoop();
#elif !defined(EMSCRIPTEN)
while (running) mainLoop();
#else
#include <emscripten.h>
//FIXME: this assumes that the display refreshes at 60Hz
emscripten_set_main_loop(mainLoop, 0, 0);
emscripten_set_main_loop_timing(EM_TIMING_RAF, 2);
return 0;
#endif
if (game_state) SDL_free(game_state);
if (initial_game_state) SDL_free(initial_game_state);
SDL_FreeSurface(gfx);
SDL_FreeSurface(font);
for (int i = 0; i < (sizeof snd) / (sizeof * snd); i++) {
if (snd[i]) Mix_FreeChunk(snd[i]);
}
for (int i = 0; i < (sizeof mus) / (sizeof * mus); i++) {
if (mus[i]) Mix_FreeMusic(mus[i]);
}
Mix_CloseAudio();
Mix_Quit();
SDL_Quit();
return 0;
}
#if SDL_MAJOR_VERSION >= 2
/* These inputs aren't sent to the game. */
enum {
PSEUDO_BTN_SAVE_STATE = 6,
PSEUDO_BTN_LOAD_STATE = 7,
PSEUDO_BTN_EXIT = 8,
PSEUDO_BTN_PAUSE = 9,
};
static void ReadGamepadInput(Uint16* out_buttons);
#endif
static void mainLoop(void) {
const Uint8* kbstate = SDL_GetKeyState(NULL);
static int reset_input_timer = 0;
//hold F9 (select+start+y) to reset
if (initial_game_state != NULL
#ifdef _3DS
&& kbstate[SDLK_LSHIFT] && kbstate[SDLK_ESCAPE] && kbstate[SDLK_F11]
#else
&& kbstate[SDLK_F9]
#endif
) {
reset_input_timer++;
if (reset_input_timer >= 30) {
reset_input_timer = 0;
//reset
OSDset("reset");
paused = 0;
Celeste_P8_load_state(initial_game_state);
Celeste_P8_set_rndseed((unsigned)(time(NULL) + SDL_GetTicks()));
Mix_HaltChannel(-1);
Mix_HaltMusic();
Celeste_P8_init();
}
}
else reset_input_timer = 0;
Uint16 prev_buttons_state = buttons_state;
buttons_state = 0;
#if SDL_MAJOR_VERSION >= 2
SDL_GameControllerUpdate();
ReadGamepadInput(&buttons_state);
//HCF QUITAR ESTO
/*
if (buttons_state != 0)
{
SDL_Rect rc;
rc.x = 0;
rc.y = 0;
SDL_FillRect(screenReal, NULL, SDL_MapRGB(screenReal->format, 255, 0, 255));
SDL_Flip(screenReal);
SDL_Delay(2000);
}
*/
if (!((prev_buttons_state >> PSEUDO_BTN_PAUSE) & 1)
&& (buttons_state >> PSEUDO_BTN_PAUSE) & 1) {
goto toggle_pause;
}
if (!((prev_buttons_state >> PSEUDO_BTN_EXIT) & 1)
&& (buttons_state >> PSEUDO_BTN_EXIT) & 1) {
goto press_exit;
}
if (!((prev_buttons_state >> PSEUDO_BTN_SAVE_STATE) & 1)
&& (buttons_state >> PSEUDO_BTN_SAVE_STATE) & 1) {
goto save_state;
}
if (!((prev_buttons_state >> PSEUDO_BTN_LOAD_STATE) & 1)
&& (buttons_state >> PSEUDO_BTN_LOAD_STATE) & 1) {
goto load_state;
}
#endif
SDL_Event ev;
while (SDL_PollEvent(&ev)) switch (ev.type) {
case SDL_QUIT: running = 0; break;
case SDL_KEYDOWN: {
#if SDL_MAJOR_VERSION >= 2
if (ev.key.repeat) break; //no key repeat
#endif
if (ev.key.keysym.sym == SDLK_ESCAPE) { //do pause
toggle_pause:
if (paused) {
// unpausing: resume audio and hide menu
Mix_Resume(-1); Mix_ResumeMusic();
paused = 0;
pause_menu_active = 0;
}
else {
// pausing: pause audio and show menu
Mix_Pause(-1); Mix_PauseMusic();
paused = 1;
pause_menu_active = 1;
pause_menu_index = 0;
pause_scaling_choice = screen_scaling_mode;
pause_screenshake_choice = enable_screenshake ? 0 : 1;
}
break;
}
else if (ev.key.keysym.sym == SDLK_DELETE) { //exit
press_exit:
running = 0;
break;
}
else if (ev.key.keysym.sym == SDLK_F11 && !(kbstate[SDLK_LSHIFT] || kbstate[SDLK_ESCAPE])) {
//if (SDL_WM_ToggleFullScreen(screenReal)) { //this doesn't work on windows..
//OSDset("toggle fullscreen");
//}
//screenReal = SDL_GetVideoSurface();
break;
}
else if (0 && ev.key.keysym.sym == SDLK_5) {
Celeste_P8__DEBUG();
break;
}
else if (ev.key.keysym.sym == SDLK_s && kbstate[SDLK_LSHIFT]) { //save state
save_state:
game_state = game_state ? game_state : SDL_malloc(Celeste_P8_get_state_size());
if (game_state) {
OSDset("save state");
Celeste_P8_save_state(game_state);
game_state_music = current_music;
}
break;
}
else if (ev.key.keysym.sym == SDLK_d && kbstate[SDLK_LSHIFT]) { //load state
load_state:
if (game_state) {
OSDset("load state");
if (paused) paused = 0, Mix_Resume(-1), Mix_ResumeMusic();
Celeste_P8_load_state(game_state);
if (current_music != game_state_music) {
Mix_HaltMusic();
current_music = game_state_music;
if (game_state_music) Mix_PlayMusic(game_state_music, -1);
}
}
break;
}
else if ( //toggle screenshake (e / L+R)
#ifdef _3DS
(ev.key.keysym.sym == SDLK_d && kbstate[SDLK_s]) || (ev.key.keysym.sym == SDLK_s && kbstate[SDLK_d])
#else
ev.key.keysym.sym == SDLK_e
#endif
) {
enable_screenshake = !enable_screenshake;
OSDset("screenshake: %s", enable_screenshake ? "on" : "off");
} break;
}
}
if (!TAS) {
//HCF: Con esto si que va!!!
//buttons_state = 255;
//HCF Si que entra aqui!!
/*
SDL_Rect rc;
rc.x = 0;
rc.y = 0;
SDL_FillRect(screenReal, NULL, SDL_MapRGB(screenReal->format, 255, 0, 0));
SDL_Flip(screenReal);
while (1);
*/
if (kbstate[SDLK_LEFT]) buttons_state |= (1 << 0);
if (kbstate[SDLK_RIGHT]) buttons_state |= (1 << 1);
if (kbstate[SDLK_UP]) buttons_state |= (1 << 2);
if (kbstate[SDLK_DOWN]) buttons_state |= (1 << 3);
if (kbstate[SDLK_z] || kbstate[SDLK_c] || kbstate[SDLK_n])
{
buttons_state |= (1 << 4);
}
if (kbstate[SDLK_x] || kbstate[SDLK_v] || kbstate[SDLK_m]) buttons_state |= (1 << 5);
}
else if (TAS && !paused) {
static int t = 0;
t++;
if (t == 1) buttons_state = 1 << 4;
else if (t > 80) {
int btn;
fscanf(TAS, "%d,", &btn);
buttons_state = btn;
}
else buttons_state = 0;
}
// === pause menu navigation (DPAD + A/Start) ===
if (paused && pause_menu_active) {
// Edge helpers: true only on the frame the button goes down.
#define EDGE_BIT(bs, prev, idx) (((((prev) >> (idx)) & 1) == 0) && (((bs) >> (idx)) & 1))
const _Bool edge_left = EDGE_BIT(buttons_state, prev_buttons_state, 0);
const _Bool edge_right = EDGE_BIT(buttons_state, prev_buttons_state, 1);
const _Bool edge_up = EDGE_BIT(buttons_state, prev_buttons_state, 2);
const _Bool edge_down = EDGE_BIT(buttons_state, prev_buttons_state, 3);
const _Bool edge_A = EDGE_BIT(buttons_state, prev_buttons_state, 4); // confirm / apply
// TODO: set START_BIT to whatever bit your input code uses for Start/pause toggle.
// Commonly this is 6, but change if yours differs.
const int START_BIT = 6;
const _Bool edge_START = EDGE_BIT(buttons_state, prev_buttons_state, START_BIT);
#undef EDGE_BIT
// We now have only 2 rows: 0=Scaling, 1=Screenshake, 2=Apply
if (edge_up) pause_menu_index = (pause_menu_index + 1) % 3; // wrap 0..2
if (edge_down) pause_menu_index = (pause_menu_index + 1) % 3;
// On "Scaling" row (row 0), change option with Left/Right
if (pause_menu_index == 0) {
if (edge_left) pause_scaling_choice = (pause_scaling_choice + 2) % 3;
if (edge_right) pause_scaling_choice = (pause_scaling_choice + 1) % 3;
}
// On "Screenshake" row (row 1), change option with Left/Right
if (pause_menu_index == 1) {
if (edge_left) pause_screenshake_choice = (pause_screenshake_choice + 1) % 2;
if (edge_right) pause_screenshake_choice = (pause_screenshake_choice + 1) % 2;
}
// A = Apply when on row 2
if (edge_A && pause_menu_index == 2) {
screen_scaling_mode = pause_scaling_choice;
enable_screenshake = pause_screenshake_choice == 0;
RecomputeScreenDstRect();
OSDset("scaling: %s", scaling_names[screen_scaling_mode]);
}
// Start = unpause (always)
if (edge_START) {
Mix_Resume(-1);
Mix_ResumeMusic();
paused = 0;
pause_menu_active = 0;
buttons_state = 0;
prev_buttons_state = 0;
}
}
if (paused) {
// draw the game frame so any old OSD pixels get painted over
Celeste_P8_draw(); // <-- add this
// Always draw the small banner
DrawPausedBanner();
if (pause_menu_active) {
const int w = 120, h = 40;
const int x = (PICO8_W - w) / 2;
// const int y = 16; // old
const int y = 26; // new, lower
// panel frame
p8_rectfill(x - 2, y - 2, x + w + 2, y + h + 2, 6);
p8_rectfill(x - 1, y - 1, x + w + 1, y + h + 1, 0);
// centered title MENU
const char* title = "menu";
int tw = 4 * (int)strlen(title);
p8_print(title, x + (w - tw) / 2, y + 2, 7);
// Scaling option
const int row0 = y + 12;
p8_print("scaling:", x + 8, row0, (pause_menu_index == 0) ? 10 : 7);
const char* val = scaling_names[pause_scaling_choice];
int vw = 4 * (int)strlen(val);
int right_half_left = x + w / 2;
int right_half_width = w / 2 - 8;
int vx = right_half_left + (right_half_width - vw) / 2;
p8_print(val, vx, row0, (pause_menu_index == 0) ? 10 : 7);
// Screenshake option
const int row1 = row0 + 10;
p8_print("screen shake:", x + 8, row1, (pause_menu_index == 1) ? 10 : 7);
const char* val_screen_shake = screenshake_names[pause_screenshake_choice];
vw = 4 * (int)strlen(val_screen_shake);
right_half_left = x + w / 2;
right_half_width = w / 2 - 8;
vx = right_half_left + (right_half_width - vw) / 2;
p8_print(val_screen_shake, vx, row1, (pause_menu_index == 1) ? 10 : 7);
const char* t = "apply";
tw = 4 * (int)strlen(t);
p8_print(t, x + (w - tw) / 2, row1 + 12, (pause_menu_index == 2) ? 10 : 7);
OSDset("scaling = %dx%d", screen_dstRect.w, screen_dstRect.h);
}
}
else {
Celeste_P8_update();
Celeste_P8_draw();
}
OSDdraw();
//HCF
SDL_Rect rc;
rc.x = 64;
rc.y = 0;
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, screen);
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, NULL, &screen_dstRect);
SDL_RenderPresent(renderer);
SDL_DestroyTexture(texture);
//SDL_FillRect(screenReal, NULL, SDL_MapRGB(screenReal->format, 0, 0, 0));
//SDL_BlitSurface(screen, NULL, screenReal, &rc);
//SDL_Flip(screenReal);
//SDL_Flip(screen);
#ifdef EMSCRIPTEN //emscripten_set_main_loop already sets the fps
SDL_Delay(1);
#elif defined(_3DS)
gspWaitForVBlank(), gspWaitForVBlank();
#else
static int t = 0;
static unsigned frame_start = 0;
unsigned frame_end = SDL_GetTicks();
unsigned frame_time = frame_end - frame_start;
unsigned target_millis;
// frame timing for 30fps is 33.333... ms, but we only have integer granularity
// so alternate between 33 and 34 ms, like [33,33,34,33,33,34,...] which averages out to 33.333...
if (t < 2) target_millis = 33;
else target_millis = 34;
if (++t == 3) t = 0;
if (frame_time < target_millis) {
SDL_Delay(target_millis - frame_time);
}
frame_start = SDL_GetTicks();
#endif
}
static int gettileflag(int, int);
static void p8_line(int, int, int, int, unsigned char);
//lots of code from https://github.com/SDL-mirror/SDL/blob/bc59d0d4a2c814900a506d097a381077b9310509/src/video/SDL_surface.c#L625
//coordinates should be scaled already
static inline void Xblit(SDL_Surface* src, SDL_Rect* srcrect, SDL_Surface* dst, SDL_Rect* dstrect, int color, int flipx, int flipy) {
assert(src && dst && !src->locked && !dst->locked);
assert(dst->format->BitsPerPixel == 32 && src->format->BitsPerPixel == 8);
SDL_Rect fulldst;
/* If the destination rectangle is NULL, use the entire dest surface */
if (!dstrect)
dstrect = (fulldst = (SDL_Rect){ 0,0,dst->w,dst->h }, &fulldst);
//dstrect = (fulldst = (SDL_Rect){ 64,0,dst->w,dst->h }, &fulldst);
//HCF
int srcx, srcy, w, h;
/* clip the source rectangle to the source surface */
if (srcrect) {
int maxw, maxh;
srcx = srcrect->x;
w = srcrect->w;
if (srcx < 0) {
w += srcx;
dstrect->x -= srcx;
srcx = 0;
}
maxw = src->w - srcx;
if (maxw < w)
w = maxw;
srcy = srcrect->y;
h = srcrect->h;
if (srcy < 0) {
h += srcy;
dstrect->y -= srcy;
srcy = 0;
}
maxh = src->h - srcy;
if (maxh < h)
h = maxh;
}
else {
srcx = srcy = 0;
w = src->w;
h = src->h;
}
/* clip the destination rectangle against the clip rectangle */
{
SDL_Rect* clip = &dst->clip_rect;
int dx, dy;
dx = clip->x - dstrect->x;
if (dx > 0) {
w -= dx;
dstrect->x += dx;
srcx += dx;
}
dx = dstrect->x + w - clip->x - clip->w;
if (dx > 0)
w -= dx;
dy = clip->y - dstrect->y;
if (dy > 0) {
h -= dy;
dstrect->y += dy;
srcy += dy;
}
dy = dstrect->y + h - clip->y - clip->h;
if (dy > 0)
h -= dy;
}
if (w && h) {
unsigned char* srcpix = src->pixels;
int srcpitch = src->pitch;
Uint32* dstpix = dst->pixels;
#define _blitter(dp, xflip) do \
for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { \
unsigned char p = srcpix[!xflip ? srcx+x+(srcy+y)*srcpitch : srcx+(w-x-1)+(srcy+y)*srcpitch]; \
if (p) dstpix[dstrect->x+x + (dstrect->y+y)*dst->w] = getcolor(dp); \
} while(0)
if (color && flipx) _blitter(color, 1);
else if (!color && flipx) _blitter(p, 1);
else if (color && !flipx) _blitter(color, 0);
else if (!color && !flipx) _blitter(p, 0);
#undef _blitter
}
}
static void p8_rectfill(int x0, int y0, int x1, int y1, int col) {
int w = (x1 - x0 + 1) * scale;
int h = (y1 - y0 + 1) * scale;
if (w > 0 && h > 0) {
SDL_Rect rc = { x0 * scale,y0 * scale, w,h };
SDL_FillRect(screen, &rc, getcolor(col));
}
}
static void p8_print(const char* str, int x, int y, int col) {
for (char c = *str; c; c = *(++str)) {
c &= 0x7F;
SDL_Rect srcrc = { 8 * (c % 16), 8 * (c / 16) };
srcrc.x *= scale;
srcrc.y *= scale;
srcrc.w = srcrc.h = 8 * scale;
SDL_Rect dstrc = { x * scale, y * scale, scale, scale };
Xblit(font, &srcrc, screen, &dstrc, col, 0, 0);
x += 4;
}
}
int pico8emu(CELESTE_P8_CALLBACK_TYPE call, ...) {
static int camera_x = 0, camera_y = 0;
if (!enable_screenshake) {
camera_x = camera_y = 0;
}
va_list args;
int ret = 0;
va_start(args, call);
#define INT_ARG() va_arg(args, int)
#define BOOL_ARG() (Celeste_P8_bool_t)va_arg(args, int)
#define RET_INT(_i) do {ret = (_i); goto end;} while (0)
#define RET_BOOL(_b) RET_INT(!!(_b))
switch (call) {
case CELESTE_P8_MUSIC: { //music(idx,fade,mask)
int index = INT_ARG();
int fade = INT_ARG();
int mask = INT_ARG();
(void)mask; //we do not care about this since sdl mixer keeps sounds and music separate
if (index == -1) { //stop playing
Mix_FadeOutMusic(fade);
current_music = NULL;
}
else if (mus[index / 10]) {
Mix_Music* musi = mus[index / 10];
current_music = musi;
Mix_FadeInMusic(musi, -1, fade);
}
} break;
case CELESTE_P8_SPR: { //spr(sprite,x,y,cols,rows,flipx,flipy)
int sprite = INT_ARG();
int x = INT_ARG();
int y = INT_ARG();
int cols = INT_ARG();
int rows = INT_ARG();
int flipx = BOOL_ARG();
int flipy = BOOL_ARG();
(void)cols;
(void)rows;
assert(rows == 1 && cols == 1);
if (sprite >= 0) {
SDL_Rect srcrc = {
8 * (sprite % 16),
8 * (sprite / 16)
};
srcrc.x *= scale;
srcrc.y *= scale;
srcrc.w = srcrc.h = scale * 8;
SDL_Rect dstrc = {
//HCF
//64+(x - camera_x)* scale, (y - camera_y)* scale,
(x - camera_x) * scale, (y - camera_y) * scale,
scale, scale
};
Xblit(gfx, &srcrc, screen, &dstrc, 0, flipx, flipy);
}
} break;
case CELESTE_P8_BTN: { //btn(b)
int b = INT_ARG();
assert(b >= 0 && b <= 5);
RET_BOOL(buttons_state & (1 << b));
} break;
case CELESTE_P8_SFX: { //sfx(id)
int id = INT_ARG();
if (id < (sizeof snd) / (sizeof * snd) && snd[id])
Mix_PlayChannel(-1, snd[id], 0);
} break;
case CELESTE_P8_PAL: { //pal(a,b)
int a = INT_ARG();
int b = INT_ARG();
if (a >= 0 && a < 16 && b >= 0 && b < 16) {
//swap palette colors
palette[a] = base_palette[b];
}
} break;
case CELESTE_P8_PAL_RESET: { //pal()
ResetPalette();
} break;
case CELESTE_P8_CIRCFILL: { //circfill(x,y,r,col)
int cx = INT_ARG() - camera_x;
int cy = INT_ARG() - camera_y;
int r = INT_ARG();
int col = INT_ARG();
int realcolor = getcolor(col);
if (r <= 1) {
SDL_FillRect(screen, &(SDL_Rect){scale* (cx - 1), scale* cy, scale * 3, scale}, realcolor);
SDL_FillRect(screen, &(SDL_Rect){scale* cx, scale* (cy - 1), scale, scale * 3}, realcolor);
}
else if (r <= 2) {
SDL_FillRect(screen, &(SDL_Rect){scale* (cx - 2), scale* (cy - 1), scale * 5, scale * 3}, realcolor);
SDL_FillRect(screen, &(SDL_Rect){scale* (cx - 1), scale* (cy - 2), scale * 3, scale * 5}, realcolor);
}
else if (r <= 3) {
SDL_FillRect(screen, &(SDL_Rect){scale* (cx - 3), scale* (cy - 1), scale * 7, scale * 3}, realcolor);
SDL_FillRect(screen, &(SDL_Rect){scale* (cx - 1), scale* (cy - 3), scale * 3, scale * 7}, realcolor);
SDL_FillRect(screen, &(SDL_Rect){scale* (cx - 2), scale* (cy - 2), scale * 5, scale * 5}, realcolor);
}
else { //i dont think the game uses this
int f = 1 - r; //used to track the progress of the drawn circle (since its semi-recursive)
int ddFx = 1; //step x
int ddFy = -2 * r; //step y
int x = 0;
int y = r;
//this algorithm doesn't account for the diameters
//so we have to set them manually
p8_line(cx, cy - y, cx, cy + r, col);
p8_line(cx + r, cy, cx - r, cy, col);
while (x < y) {
if (f >= 0) {
y--;
ddFy += 2;
f += ddFy;
}
x++;
ddFx += 2;
f += ddFx;
//build our current arc
p8_line(cx + x, cy + y, cx - x, cy + y, col);
p8_line(cx + x, cy - y, cx - x, cy - y, col);
p8_line(cx + y, cy + x, cx - y, cy + x, col);
p8_line(cx + y, cy - x, cx - y, cy - x, col);
}
}
} break;
case CELESTE_P8_PRINT: { //print(str,x,y,col)
const char* str = va_arg(args, const char*);
int x = INT_ARG() - camera_x;
int y = INT_ARG() - camera_y;
int col = INT_ARG() % 16;
#ifdef _3DS
if (!strcmp(str, "x+c")) {
//this is confusing, as 3DS uses a+b button, so use this hack to make it more appropiate
str = "a+b";
}
#endif
p8_print(str, x, y, col);
} break;
case CELESTE_P8_RECTFILL: { //rectfill(x0,y0,x1,y1,col)
int x0 = INT_ARG() - camera_x;
int y0 = INT_ARG() - camera_y;
int x1 = INT_ARG() - camera_x;
int y1 = INT_ARG() - camera_y;
int col = INT_ARG();
p8_rectfill(x0, y0, x1, y1, col);
} break;
case CELESTE_P8_LINE: { //line(x0,y0,x1,y1,col)
int x0 = INT_ARG() - camera_x;
int y0 = INT_ARG() - camera_y;
int x1 = INT_ARG() - camera_x;
int y1 = INT_ARG() - camera_y;
int col = INT_ARG();
p8_line(x0, y0, x1, y1, col);
} break;
case CELESTE_P8_MGET: { //mget(tx,ty)
int tx = INT_ARG();
int ty = INT_ARG();
RET_INT(tilemap_data[tx + ty * 128]);
} break;
case CELESTE_P8_CAMERA: { //camera(x,y)
if (enable_screenshake) {
camera_x = INT_ARG();
camera_y = INT_ARG();
}
} break;
case CELESTE_P8_FGET: { //fget(tile,flag)
int tile = INT_ARG();
int flag = INT_ARG();
RET_INT(gettileflag(tile, flag));
} break;
case CELESTE_P8_MAP: { //map(mx,my,tx,ty,mw,mh,mask)
int mx = INT_ARG(), my = INT_ARG();
int tx = INT_ARG(), ty = INT_ARG();
int mw = INT_ARG(), mh = INT_ARG();
int mask = INT_ARG();
for (int x = 0; x < mw; x++) {
for (int y = 0; y < mh; y++) {
int tile = tilemap_data[x + mx + (y + my) * 128];
//hack
if (mask == 0 || (mask == 4 && tile_flags[tile] == 4) || gettileflag(tile, mask != 4 ? mask - 1 : mask)) {
SDL_Rect srcrc = {
8 * (tile % 16),
8 * (tile / 16)
};
srcrc.x *= scale;
srcrc.y *= scale;
srcrc.w = srcrc.h = scale * 8;
SDL_Rect dstrc = {
//HCF
//64+(tx + x * 8 - camera_x)* scale, (ty + y * 8 - camera_y)* scale,
(tx + x * 8 - camera_x) * scale, (ty + y * 8 - camera_y) * scale,
scale * 8, scale * 8
};
if (0) {
srcrc.x = srcrc.y = 0;
srcrc.w = srcrc.h = 8;
dstrc.x = x * 8, dstrc.y = y * 8;
dstrc.w = dstrc.h = 8;
}
Xblit(gfx, &srcrc, screen, &dstrc, 0, 0, 0);
}
}
}
} break;
}
end:
va_end(args);
return ret;
}
static int gettileflag(int tile, int flag) {
return tile < sizeof(tile_flags) / sizeof(*tile_flags) && (tile_flags[tile] & (1 << flag)) != 0;
}
//coordinates should NOT be scaled before calling this
static void p8_line(int x0, int y0, int x1, int y1, unsigned char color) {
#define CLAMP(v,min,max) v = v < min ? min : v >= max ? max-1 : v;
CLAMP(x0, 0, screen->w);
CLAMP(y0, 0, screen->h);
CLAMP(x1, 0, screen->w);
CLAMP(y1, 0, screen->h);
Uint32 realcolor = getcolor(color);
#undef CLAMP
#define PLOT(x,y) do { \
SDL_FillRect(screen, &(SDL_Rect){x*scale,y*scale,scale,scale}, realcolor); \
} while (0)
int sx, sy, dx, dy, err, e2;
dx = abs(x1 - x0);
dy = abs(y1 - y0);
if (!dx && !dy) return;
if (x0 < x1) sx = 1; else sx = -1;
if (y0 < y1) sy = 1; else sy = -1;
err = dx - dy;
if (!dy && !dx) return;
else if (!dx) { //vertical line
for (int y = y0; y != y1; y += sy) PLOT(x0, y);
}
else if (!dy) { //horizontal line
for (int x = x0; x != x1; x += sx) PLOT(x, y0);
} while (x0 != x1 || y0 != y1) {
PLOT(x0, y0);
e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
#undef PLOT
}
#if SDL_MAJOR_VERSION >= 2
//SDL2: read input from connected gamepad
struct mapping {
SDL_GameControllerButton sdl_btn;
Uint16 pico8_btn;
};
static const char* pico8_btn_names[] = {
"left", "right", "up", "down", "jump", "dash", "save", "load", "exit", "pause"
};
// initialized with default mapping
static struct mapping controller_mappings[30] = {
{SDL_CONTROLLER_BUTTON_DPAD_LEFT, 0}, //left
{SDL_CONTROLLER_BUTTON_DPAD_RIGHT, 1}, //right
{SDL_CONTROLLER_BUTTON_DPAD_UP, 2}, //up
{SDL_CONTROLLER_BUTTON_DPAD_DOWN, 3}, //down
{SDL_CONTROLLER_BUTTON_A, 4}, //jump
{SDL_CONTROLLER_BUTTON_B, 5}, //dash
{SDL_CONTROLLER_BUTTON_LEFTSHOULDER, PSEUDO_BTN_SAVE_STATE}, //save
{SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, PSEUDO_BTN_LOAD_STATE}, //load
{SDL_CONTROLLER_BUTTON_GUIDE, PSEUDO_BTN_EXIT}, //exit
{SDL_CONTROLLER_BUTTON_START, PSEUDO_BTN_PAUSE}, //pause
{0xff, 0xff}
};
static const Uint16 stick_deadzone = 32767 / 2; //about half
static void ReadGamepadInput(Uint16* out_buttons) {
*out_buttons = 0;
SDL_PumpEvents();
SDL_JoystickUpdate(); //manual refresh of the gamepad(s)
static _Bool read_config = 0;
if (!read_config) {
read_config = 1;
const char* cfg_file_path = "D:\\ccleste-input-cfg.txt";
FILE* cfg = fopen(cfg_file_path, "r");
if (cfg) {
int i;
for (i = 0; i < sizeof controller_mappings / sizeof *controller_mappings - 1;) {
char line[150], p8btn[31], sdlbtn[31];
fgets(line, sizeof line - 1, cfg);
if (feof(cfg) || ferror(cfg)) break;
line[sizeof line - 1] = 0;
if (*line == '#') {
//comment
} else if (sscanf(line, "%30s %30s", p8btn, sdlbtn) == 2) {
p8btn[sizeof p8btn - 1] = sdlbtn[sizeof sdlbtn - 1] = 0;
for (int btn = 0; btn < sizeof pico8_btn_names / sizeof *pico8_btn_names; btn++) {
if (!SDL_strcasecmp(pico8_btn_names[btn], p8btn)) {
//printf("input cfg: %s -> %s\n", p8btn, sdlbtn);
controller_mappings[i].sdl_btn = SDL_GameControllerGetButtonFromString(sdlbtn);
controller_mappings[i].pico8_btn = btn;
i++;
}
}
}
}
controller_mappings[i].pico8_btn = 0xFF;
fclose(cfg);
} else {
cfg = fopen(cfg_file_path, "w");
if (cfg) {
//printf("creating ccleste-input-cfg.txt with default button mappings\n");
fprintf(cfg, "# in-game \tcontroller\n");
for (struct mapping* mapping = controller_mappings; mapping->pico8_btn != 0xFF; mapping++) {
fprintf(cfg, "%s \t%s\n", pico8_btn_names[mapping->pico8_btn], SDL_GameControllerGetStringForButton(mapping->sdl_btn));
}
fclose(cfg);
}
}
}
static SDL_GameController* controller = NULL;
if (!controller) {
static int tries_left = 30;
if (!tries_left) return;
tries_left--;
//use first available controller
int count = SDL_NumJoysticks();
//printf("sdl reports %i controllers\n", count);
for (int i = 0; i < count; i++) {
if (SDL_IsGameController(i)) {
controller = SDL_GameControllerOpen(i);
if (!controller) {
//fprintf(stderr, "error opening controller: %s\n", SDL_GetError());
return;
}
//printf("picked controller: '%s'\n", SDL_GameControllerName(controller));
break;
}
}
}
//pico 8 buttons and pseudo buttons
for (int i = 0; i < sizeof controller_mappings / sizeof *controller_mappings; i++) {
struct mapping mapping = controller_mappings[i];
if (mapping.pico8_btn == 0xFF) break;
_Bool pressed = SDL_GameControllerGetButton(controller, mapping.sdl_btn);
Uint16 mask = ~(1 << mapping.pico8_btn);
*out_buttons = (*out_buttons & mask) | (pressed << mapping.pico8_btn);
}
}
#endif
// vim: ts=2 sw=2 noexpandtab