#define SDL_MAIN_HANDLED #include #include #include #include #include "input.h" #include "loader.h" #include "util.h" #include "tilemap.h" #include "incbin.h" #include "libplum.h" #include "main.h" #include SDL_Window *window = NULL; SDL_Renderer *renderer = NULL; enum game_state game_state = STATE_PLAYING; char *game_next_level; static void *particle_tex = NULL; #define WINDOW_WIDTH 160 #define WINDOW_HEIGHT 90 #define INIT_SUBSYSTEMS SDL_INIT_VIDEO unsigned input_pressed = 0, input_held = 0; SDL_Scancode keybinds[] = { SDL_SCANCODE_UP, SDL_SCANCODE_DOWN, SDL_SCANCODE_LEFT, SDL_SCANCODE_RIGHT, SDL_SCANCODE_A, SDL_SCANCODE_S, }; struct entities entities = {{{0}}, {{0}}, 0, {{0}}, 0}, next_entities = {{{0}}, {{0}}, 0, {{0}}, 0}; void *player_new(struct entities *entities); int player_property(void *const restrict entity, char const *const restrict property, char const *const restrict value); void funs_init(void); void entities_free(struct entities *entities) { for (int i = 0, e = 0; i < 64 && e < entities->enemies; i++) { if (entities->enemy[i].state) { e++; if (entities->enemy[i].free != NULL) { entities->enemy[i].free(entities->enemy + i); } } } entities->enemies = 0; #if 0 for (int i = 0; i < entities.warps; i++) { entities.warp[i].update(entities.warp + i); } #endif entities->warps = 0; } int entities_load(struct entities *entities, char *data, size_t size, size_t input_bytes) { entities->enemies = 0; entities->warps = 0; entities->projectiles = 0; entities->particles = 0; data += input_bytes; size -= input_bytes; while (size) { size_t len; if ((len = strnlen(data, size)) == size) { return 1; } char *name = data; struct funs fun = res_get_fun(name); data += len + 1; size -= len + 1; if (fun.newfun) { struct entity *entity = fun.newfun(&next_entities); while (1) { if ((len = strnlen(data, size)) == size) { return 1; } else if (len == 0) { data++; size--; break; } char *key = data; data += len + 1; size -= len + 1; if ((len = strnlen(data, size)) == size) { return 1; } char *value = data; data += len + 1; size -= len + 1; if (fun.setfun(entity, key, value)) { puts(key); puts(value); return 1; } } } else { fprintf(stderr, "warn: unknown entity %s\n", name); while (1) { if ((len = strnlen(data, size)) == size) { return 1; } else if (len == 0) { data++; size--; break; } data += len + 1; size -= len + 1; if ((len = strnlen(data, size)) == size) { return 1; } data += len + 1; size -= len + 1; } } } return 0; } int game_update(void) { if (entities.player[0].update(entities.player)) { return 1; } for (int i = 0, e = 0; i < 64 && e < entities.enemies; i++) { if (entities.enemy[i].state) { e++; entities.enemy[i].update(entities.enemy + i); } } for (int i = 0; i < entities.warps; i++) { entities.warp[i].update(entities.warp + i); } for (int i = 0, e = 0; i < 64 && e < entities.projectiles; i++) { if (entities.projectile[i].state) { e++; if (entities.projectile[i].update(entities.projectile + i)) { entities.projectiles--; if (entities.projectile + i != entities.projectile + entities.projectiles) { memcpy(entities.projectile + i, entities.projectile + entities.projectiles, sizeof (struct projectile)); } continue; } } } for (int i = 0; i < entities.particles; i++) { if (entities.particle[i].hp-- == 0) { entities.particles--; if (entities.particle + i != entities.particle + entities.particles) { memcpy(entities.particle + i, entities.particle + entities.particles, sizeof (struct particle)); } continue; } entities.particle[i].x += (entities.particle[i].velocity.x += entities.particle[i].acceleration.x); entities.particle[i].y += (entities.particle[i].velocity.y += entities.particle[i].acceleration.y); } return 0; } void game_render(SDL_Texture *framebuffer, int x, int y) { SDL_SetRenderTarget(renderer, framebuffer); tilemap_background(tilemap, x, y, WINDOW_WIDTH, WINDOW_HEIGHT); entities.player[0].draw(entities.player, x, y); for (int i = 0, e = 0; i < 64 && e < entities.enemies; i++) { if (entities.enemy[i].state) { e++; entities.enemy[i].draw(entities.enemy + i, x, y); } } for (int i = 0, e = 0; i < 64 && e < entities.projectiles; i++) { if (entities.projectile[i].state) { e++; entities.projectile[i].draw(entities.projectile + i, x, y); } } for (int i = 0; i < entities.particles; i++) { struct particle *self = entities.particle + i; SDL_RenderCopy(renderer, particle_tex, &self->rect, &(SDL_Rect) {from_fixed(self->x) - x, from_fixed(self->y) - y, self->rect.w, self->rect.h}); } tilemap_foreground(tilemap, x, y, WINDOW_WIDTH, WINDOW_HEIGHT); } void game_render_flush(SDL_Texture *framebuffer) { SDL_SetRenderTarget(renderer, NULL); SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0); SDL_RenderClear(renderer); SDL_RenderCopy(renderer, framebuffer, &(SDL_Rect) {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}, &(SDL_Rect) {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}); // then we wait for the next video frame SDL_RenderPresent(renderer); } int main(int const argc, char *const *const argv) { struct option opts[] = { {"help", 0, NULL, 'h'}, {"scale", 1, NULL, 's'}, {"resizable", 2, NULL, 'r'}, {"fullscreen", 2, NULL, 'f'}, {"maximize", 2, NULL, 'm'}, {"border", 2, NULL, 'b'}, {NULL, 0, NULL, 0 }, }; int opt, li, scale = 0; unsigned flags = 0, error = 0; while ((opt = getopt_long(argc, argv, "h", opts, &li)) != -1) { switch (opt) { case 'h': printf("usage:\n\t%s\n\t%s [mod.zip]\n", argv[0], argv[0]); return 0; case 's': scale = atoi(optarg); break; case 'r': if (optarg == NULL || strcmp(optarg, "true") == 0) { flags |= SDL_WINDOW_RESIZABLE; } else if (strcmp(optarg, "false") == 0) { flags &= ~SDL_WINDOW_RESIZABLE; } else { fprintf(stderr, "%s=%s -- expected 'true' or 'false'\n", opts[li].name, optarg); error = 1; } break; case 'f': if (optarg == NULL || strcmp(optarg, "true") == 0) { flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; } else if (strcmp(optarg, "false") == 0) { flags &= ~SDL_WINDOW_FULLSCREEN_DESKTOP; } else { fprintf(stderr, "%s=%s -- expected 'true' or 'false'\n", opts[li].name, optarg); error = 1; } break; case 'm': if (optarg == NULL || strcmp(optarg, "true") == 0) { flags |= SDL_WINDOW_MAXIMIZED; } else if (strcmp(optarg, "false") == 0) { flags &= ~SDL_WINDOW_MAXIMIZED; } else { fprintf(stderr, "%s=%s -- expected 'true' or 'false'\n", opts[li].name, optarg); error = 1; } break; case 'b': if (optarg == NULL || strcmp(optarg, "true") == 0) { flags &= ~SDL_WINDOW_BORDERLESS; } else if (strcmp(optarg, "false") == 0) { flags |= SDL_WINDOW_BORDERLESS; } else { fprintf(stderr, "%s=%s -- expected 'true' or 'false'\n", opts[li].name, optarg); error = 1; } break; default: error = 1; } } if (error) { return EXIT_FAILURE; } if (SDL_Init(INIT_SUBSYSTEMS)) { fprintf(stderr, "failed to initialize SDL2: %s\n", SDL_GetError()); return EXIT_FAILURE; } SDL_StopTextInput(); if (scale <= 0) { // this looks very wrong SDL_DisplayMode dm; if (SDL_GetDesktopDisplayMode(0, &dm) != 0) { SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "couldnt get desktop size", SDL_GetError(), NULL); fprintf(stderr, "info: couldnt get desktop size %s\n", SDL_GetError()); flags |= SDL_WINDOW_RESIZABLE; } else { int x = dm.w / 2 / WINDOW_WIDTH; int y = dm.h / 2 / WINDOW_HEIGHT; if (x < y) { scale = x; } else { scale = y; } if (scale == 0) { scale = 1; } if (dm.refresh_rate != 60) { SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_WARNING, "refresh rate", "this game currently only runs well on 60Hz displays", NULL); } } } window = SDL_CreateWindow(":3", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_WIDTH * scale, WINDOW_HEIGHT * scale, flags | SDL_WINDOW_HIDDEN); if (window == NULL) { goto end; } SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl"); // hack, i dont wanna deal with windows discarding render textures renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_TARGETTEXTURE | SDL_RENDERER_PRESENTVSYNC); if (renderer == NULL) { goto end; } SDL_RenderSetLogicalSize(renderer, WINDOW_WIDTH, WINDOW_HEIGHT); SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); SDL_Texture *framebuffer = SDL_CreateTexture(renderer, SDL_PIXELTYPE_UNKNOWN, SDL_TEXTUREACCESS_TARGET, WINDOW_WIDTH, WINDOW_HEIGHT); { res_init(); funs_init(); if (optind == argc) { void *a = util_executableRelativePath("assets.res", *argv, 0); puts(a); if (loadResources(a)) { fputs("loading resources failed\n", stderr); return 1; } free(a); } else { while (optind < argc) { if (loadResources(argv[optind])) { fprintf(stderr, "loading %s failed\n", argv[optind]); return 1; } optind++; } } particle_tex = res_get_texture("particles").data; struct blob blob = res_get_map("untitled"); next_tilemap = tilemap_load(blob.data, blob.size); if (next_tilemap == NULL) { fputs("failed to load initial tilemap\n", stderr); goto end; } if (entities_load(&entities, blob.data, blob.size, next_tilemap->input_bytes)) { tilemap_free(next_tilemap); fputs("failed to load initial tilemap\n", stderr); goto end; } tilemap = next_tilemap; } /* unsigned error; // identical variable is declared higher up */ struct plum_image *image = plum_load_image(game_icon, game_icon_end - game_icon, PLUM_COLOR_32 | PLUM_ALPHA_INVERT, &error); if (image == NULL) { fprintf(stderr, "error: libplum: %s\n", plum_get_error_text(error)); goto icon_done; } SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(image->data, image->width, image->height, 32, image->width * 4, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000); if (surface == NULL) { plum_destroy_image(image); fprintf(stderr, "error: SDL2: %s\n", SDL_GetError()); goto icon_done; } SDL_SetWindowIcon(window, surface); SDL_FreeSurface(surface); plum_destroy_image(image); icon_done: SDL_ShowWindow(window); int x = 0, y = 0, fade = 0; player_new(&next_entities); player_property(next_entities.player, "x", "40"); player_property(next_entities.player, "y", "64"); memcpy(&entities, &next_entities, sizeof (entities)); while (1) { input_pressed = input_held; SDL_Event evt; while (SDL_PollEvent(&evt)) { switch (evt.type) { case SDL_QUIT: // the last window is closed, or the app is asked to terminate fputs("quitting...\n", stderr); goto end; case SDL_KEYDOWN: case SDL_KEYUP: // a key on is pressed or released if (evt.key.repeat) break; // check for ^Q and exit if pressed if (evt.key.state == SDL_PRESSED && evt.key.keysym.sym == SDLK_q && evt.key.keysym.mod & KMOD_CTRL) { goto end; } //static_assert(INPUT_LENGTH <= sizeof(input_held) * CHAR_BIT); // if this trips up, scope creep happened for (unsigned key = 0, bit = 1; key < INPUT_LENGTH; key++, bit <<= 1) { if (evt.key.keysym.scancode == keybinds[key]) { if (evt.key.state == SDL_PRESSED) input_held |= bit; else input_held &= ~bit; } } //fprintf(stderr, "input: %0*b\n", INPUT_LENGTH, input_held); break; case SDL_MOUSEMOTION: // the cursor moves //fprintf(stderr, "mouse at %4i %4i\n", evt.motion.x, evt.motion.y); break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: // a left click or release if (evt.button.state) { //fprintf(stderr, "click %i\n", evt.button.button); } if (!evt.button.state) { //fprintf(stderr, "unclick %i\n", evt.button.button); } break; case SDL_MOUSEWHEEL: // the scroll wheel gets rolled //fprintf(stderr, "scrolled by %2i %2i %4.1f %4.1f\n", evt.wheel.x, evt.wheel.y, evt.wheel.preciseX, evt.wheel.preciseY); break; case SDL_CLIPBOARDUPDATE: // annoying break; case SDL_WINDOWEVENT: // window related events switch (evt.window.event) { case SDL_WINDOWEVENT_SHOWN: //fprintf(stderr, "window %u shown\n", evt.window.windowID); break; case SDL_WINDOWEVENT_HIDDEN: //fprintf(stderr, "window %u hidden\n", evt.window.windowID); break; case SDL_WINDOWEVENT_EXPOSED: //fprintf(stderr, "window %u exposed\n", evt.window.windowID); break; case SDL_WINDOWEVENT_MOVED: // a window is moved //fprintf(stderr, "window %u moved %4i %4i\n", evt.window.windowID, evt.window.data1, evt.window.data2); break; case SDL_WINDOWEVENT_RESIZED: // a window is resized //fprintf(stderr, "window %u resized %4i %4i\n", evt.window.windowID, evt.window.data1, evt.window.data2); break; case SDL_WINDOWEVENT_SIZE_CHANGED: // ? break; case SDL_WINDOWEVENT_MINIMIZED: //fprintf(stderr, "window %u minimized\n", evt.window.windowID); break; case SDL_WINDOWEVENT_MAXIMIZED: //fprintf(stderr, "window %u maximized\n", evt.window.windowID); break; case SDL_WINDOWEVENT_RESTORED: //fprintf(stderr, "window %u restored\n", evt.window.windowID); break; case SDL_WINDOWEVENT_ENTER: case SDL_WINDOWEVENT_LEAVE: // cursor enters or leaves a window case SDL_WINDOWEVENT_FOCUS_GAINED: case SDL_WINDOWEVENT_FOCUS_LOST: // keyboard break; case SDL_WINDOWEVENT_CLOSE: // a window is asked to close //fprintf(stderr, "window %u closed\n", evt.window.windowID); break; case SDL_WINDOWEVENT_TAKE_FOCUS: //fprintf(stderr, "window %u offered focus\n", evt.window.windowID); break; case SDL_WINDOWEVENT_HIT_TEST: //fprintf(stderr, "window %u hit test\n", evt.window.windowID); break; default: fprintf(stderr, "window %u event %i\n", evt.window.windowID, evt.window.event); } break; default: fprintf(stderr, "unknown event type %i\n", evt.type); } } input_pressed = ~input_pressed & input_held; //fprintf(stderr, "input: %0*b\n", INPUT_LENGTH, input_pressed); switch (game_state) { case STATE_PLAYING: //SDL_RenderCopy(renderer, tilemap->wang_tileset, &(SDL_Rect) {0, 0, 128, 90}, &(SDL_Rect) {0, 0, 128, 90}); if (game_update()) { goto end; // TODO: game over state } x = (entities.player[0].x / 16) - (WINDOW_WIDTH / 2); y = (entities.player[0].y / 16) - (WINDOW_HEIGHT / 2); if (x < 0) {x = 0;} else if (x + WINDOW_WIDTH > tilemap->width * 8) {x = tilemap->width * 8 - WINDOW_WIDTH;} if (y < 0) {y = 0;} else if (y + WINDOW_HEIGHT > tilemap->height * 8) {y = tilemap->height * 8 - WINDOW_HEIGHT;} game_render(framebuffer, x, y); game_render_flush(framebuffer); break; case STATE_FADE_IN: fade += 15; SDL_SetRenderTarget(renderer, NULL); SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0); SDL_RenderClear(renderer); SDL_RenderCopy(renderer, framebuffer, &(SDL_Rect) {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}, &(SDL_Rect) {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}); SDL_SetRenderDrawColor(renderer, 0, 0, 0, fade); SDL_RenderFillRect(renderer, &(SDL_Rect) {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}); SDL_RenderPresent(renderer); if (fade == 255) { struct blob blob = res_get_map(game_next_level); next_tilemap = tilemap_load(blob.data, blob.size); if (next_tilemap != NULL) { if (entities_load(&next_entities, blob.data, blob.size, next_tilemap->input_bytes)) { tilemap_free(next_tilemap); } else { tilemap_free(tilemap); tilemap = next_tilemap; entities_free(&entities); memcpy(&entities, &next_entities, sizeof (entities)); } } game_state = STATE_FADE_OUT; x = (entities.player[0].x / 16) - (WINDOW_WIDTH / 2); y = (entities.player[0].y / 16) - (WINDOW_HEIGHT / 2); if (x < 0) {x = 0;} else if (x + WINDOW_WIDTH > tilemap->width * 8) {x = tilemap->width * 8 - WINDOW_WIDTH;} if (y < 0) {y = 0;} else if (y + WINDOW_HEIGHT > tilemap->height * 8) {y = tilemap->height * 8 - WINDOW_HEIGHT;} game_render(framebuffer, x, y); SDL_SetRenderTarget(renderer, NULL); SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0); SDL_RenderClear(renderer); } break; case STATE_FADE_OUT: fade -= 15; SDL_SetRenderTarget(renderer, NULL); SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0); SDL_RenderClear(renderer); SDL_RenderCopy(renderer, framebuffer, &(SDL_Rect) {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}, &(SDL_Rect) {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}); SDL_SetRenderDrawColor(renderer, 0, 0, 0, fade); SDL_RenderFillRect(renderer, &(SDL_Rect) {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}); SDL_RenderPresent(renderer); if (fade == 0) { game_state = STATE_PLAYING; } break; } } end: tilemap_free(tilemap), tilemap = NULL; SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); //SDL_QuitSubSystem(INIT_SUBSYSTEMS); return 0; }